Оставаться сухим с JAX-RS - PullRequest
60 голосов
/ 04 мая 2011

Я пытаюсь свести к минимуму повторяющийся код для ряда обработчиков ресурсов JAX-RS, каждый из которых требует несколько одинаковых путей и параметров запроса.Шаблон базового URL для каждого ресурса выглядит следующим образом:

/{id}/resourceName

, и каждый ресурс имеет несколько подресурсов:

/{id}/resourceName/subresourceName

Таким образом, пути к ресурсам / подресурсам (включая параметры запроса) могут выглядетькак

/12345/foo/bar?xyz=0
/12345/foo/baz?xyz=0
/12345/quux/abc?xyz=0
/12345/quux/def?xyz=0

Общими частями ресурсов foo и quux являются @PathParam("id") и @QueryParam("xyz")мог бы реализовать классы ресурсов следующим образом:

// FooService.java
@Path("/{id}/foo")
public class FooService
{
    @PathParam("id") String id;
    @QueryParam("xyz") String xyz;

    @GET @Path("bar")
    public Response getBar() { /* snip */ }

    @GET @Path("baz")
    public Response getBaz() { /* snip */ }
}

// QuuxService.java
@Path("/{id}/quux")
public class QuxxService
{
    @PathParam("id") String id;
    @QueryParam("xyz") String xyz;

    @GET @Path("abc")
    public Response getAbc() { /* snip */ }

    @GET @Path("def")
    public Response getDef() { /* snip */ }
}

Мне удалось избежать повторения внедрения параметра в каждый метод get*. 1 Это хорошее начало, но я также хотел бы избежать повторения для всех классов ресурсов.Подход, который работает с CDI (который мне также нужен), заключается в использовании базового класса abstract, который FooService и QuuxService может extend:

// BaseService.java
public abstract class BaseService
{
    // JAX-RS injected fields
    @PathParam("id") protected String id;
    @QueryParam("xyz") protected String xyz;

    // CDI injected fields
    @Inject protected SomeUtility util;
}

// FooService.java
@Path("/{id}/foo")
public class FooService extends BaseService
{
    @GET @Path("bar")
    public Response getBar() { /* snip */ }

    @GET @Path("baz")
    public Response getBaz() { /* snip */ }
}

// QuuxService.java
@Path("/{id}/quux")
public class QuxxService extends BaseService
{   
    @GET @Path("abc")
    public Response getAbc() { /* snip */ }

    @GET @Path("def")
    public Response getDef() { /* snip */ }
}

Внутри методов get* инъекция CDI (чудесным образом) работает правильно: поле util не равно нулю.К сожалению, инъекция JAX-RS работает , а не ;id и xyz равны null в get* методах FooService и QuuxService.

Есть ли исправление или обходной путь для этой проблемы?

Учитывая, чтоCDI работает так, как мне хотелось бы, мне интересно, является ли сбой при внедрении @PathParam s (и т. д.) в подклассы ошибкой или просто частью спецификации JAX-RS.


Другой подход, который я уже попробовал, - это использование BaseService в качестве единой точки входа, которая делегирует FooService и QuuxService по мере необходимости.Это в основном как описано в RESTful Java с JAX-RS с использованием локаторов подресурсов.

// BaseService.java
@Path("{id}")
public class BaseService
{
    @PathParam("id") protected String id;
    @QueryParam("xyz") protected String xyz;
    @Inject protected SomeUtility util;

    public BaseService () {} // default ctor for JAX-RS

    // ctor for manual "injection"
    public BaseService(String id, String xyz, SomeUtility util)
    {
        this.id = id;
        this.xyz = xyz;
        this.util = util;
    }

    @Path("foo")
    public FooService foo()
    {
        return new FooService(id, xyz, util); // manual DI is ugly
    }

    @Path("quux")
    public QuuxService quux()
    {
        return new QuuxService(id, xyz, util); // yep, still ugly
    }
}

// FooService.java
public class FooService extends BaseService
{
    public FooService(String id, String xyz, SomeUtility util)
    {
        super(id, xyz, util); // the manual DI ugliness continues
    }

    @GET @Path("bar")
    public Response getBar() { /* snip */ }

    @GET @Path("baz")
    public Response getBaz() { /* snip */ }
}

// QuuxService.java
public class QuuzService extends BaseService
{
    public FooService(String id, String xyz, SomeUtility util)
    {
        super(id, xyz, util); // the manual DI ugliness continues
    }

    @GET @Path("abc")
    public Response getAbc() { /* snip */ }

    @GET @Path("def")
    public Response getDef() { /* snip */ }
}

недостатком этого подхода является то, что ни инъекция CDI, ни инъекция JAX-RS не работают в подресурсных классах.Причина этого довольно очевидна 2 , но то, что означает , заключается в том, что мне нужно вручную повторно вводить поля в конструктор подклассов, что является грязным, уродливым иПозвольте мне настроить дальнейшую инъекцию.Пример: скажем, я хотел @Inject экземпляр в FooService, но не QuuxService.Поскольку я явно создаю экземпляр для подклассов BaseService, внедрение CDI не будет работать, поэтому уродство продолжается.


tl; dr Как правильно избегать многократного внедрения полей через JAX-Классы обработчиков ресурсов RS?

И почему JAX-RS не вводит унаследованные поля, в то время как у CDI нет проблем с этим?


Редактировать 1

Снемного направления от @ Tarlog , я думаю, что нашел ответ на один из моих вопросов,

Почему JAX-RS не вводит унаследованные поля?

In JSR-311 §3.6 :

Если у подкласса или метода реализации есть какие-либо аннотации JAX-RS, тогда all аннотации к суперклассу или интерфейсному методу игнорируются.

Я уверен, что есть реальная причина для этого решения, но, к сожалению, этот факт работает против меня в данном конкретном случае использования.Я все еще интересуюсь любыми возможными обходными путями.


1 Предостережение с использованием внедрения на уровне поля состоит в том, что я теперь привязан к классу ресурсов для каждого запросареализации, но я могу жить с этим.
2 Потому что я тот, кто вызывает new FooService(), а не контейнер / реализацию JAX-RS.

Ответы [ 8 ]

6 голосов
/ 09 сентября 2013

Вот обходной путь, который я использую:

Определите конструктор для BaseService с параметрами 'id' и 'xyz' в качестве параметров:

// BaseService.java
public abstract class BaseService
{
    // JAX-RS injected fields
    protected final String id;
    protected final String xyz;

    public BaseService (String id, String xyz) {
        this.id = id;
        this.xyz = xyz;
    }
}

Повторите конструктор для всех подклассовс инъекциями:

// FooService.java
@Path("/{id}/foo")
public class FooService extends BaseService
{
    public FooService (@PathParam("id") String id, @QueryParam("xyz") String xyz) {
        super(id, xyz);
    }

    @GET @Path("bar")
    public Response getBar() { /* snip */ }

    @GET @Path("baz")
    public Response getBaz() { /* snip */ }
}
4 голосов
/ 10 мая 2011

Глядя на JIRA Jax , кажется, что кто-то запросил наследование аннотации как веху для JAX-RS.

Функция, которую вы ищете, пока отсутствует в JAX-RSОднако будет ли это работать?Это некрасиво, но предотвращает повторные инъекции.

public abstract class BaseService
{
    // JAX-RS injected fields
    @PathParam("id") protected String id;
    @QueryParam("xyz") protected String xyz;

    // CDI injected fields
    @Inject protected SomeUtility util;

    @GET @Path("bar")
    public abstract Response getBar();

    @GET @Path("baz")
    public abstract Response getBaz();

    @GET @Path("abc")
    public abstract Response getAbc();

    @GET @Path("def")
    public abstract Response getDef();
}

// FooService.java
@Path("/{id}/foo")
public class FooService extends BaseService
{
    public Response getBar() { /* snip */ }

    public Response getBaz() { /* snip */ }
}

// QuuxService.java
@Path("/{id}/quux")
public class QuxxService extends BaseService
{   
    public Response getAbc() { /* snip */ }

    public Response getDef() { /* snip */ }
}

Или в другом обходном пути:

public abstract class BaseService
{
    @PathParam("id") protected String id;
    @QueryParam("xyz") protected String xyz;

    // CDI injected fields
    @Inject protected SomeUtility util;

    @GET @Path("{stg}")
    public abstract Response getStg(@Pathparam("{stg}") String stg);

}

// FooService.java
@Path("/{id}/foo")
public class FooService extends BaseService
{
    public Response getStg(String stg) {
        if(stg.equals("bar")) {
              return getBar();
        } else {
            return getBaz();
        }
    }
    public Response getBar() { /* snip */ }

    public Response getBaz() { /* snip */ }
}

Но, видя, насколько ты обидчив, честно говоря, я сомневаюсь, что твое разочарование исчезнет с этим уродливым кодом:)

3 голосов
/ 31 марта 2015

У меня всегда было чувство, что наследование аннотаций делает мой код нечитаемым, так как неясно, откуда и как он внедряется (например, на каком уровне дерева наследования он будет внедрен и где он был переопределен (или былэто переопределено вообще)).Более того, вы должны сделать переменную защищенной (и, вероятно, НЕ финальной), которая заставляет суперкласс утекать в его внутреннее состояние, а также может вносить некоторые ошибки (по крайней мере, я всегда спрашиваю себя при вызове расширенного метода: изменена ли защищенная переменная там?).ИМХО, это не имеет ничего общего с СУХОЙ, так как это не инкапсуляция логики, а инкапсуляция инъекций, что мне кажется преувеличенным.

В конце я приведу в спецификации JAX-RS: 3.6 Наследование аннотаций

Для соответствия другим спецификациям Java EE рекомендуется всегдаповторять аннотации вместо того, чтобы полагаться на наследование аннотаций.

PS: я допускаю, что я использую только иногда наследование аннотаций, но на уровне метода:)

3 голосов
/ 07 января 2013

В RESTEasy можно создать класс, аннотировать с помощью @ * Param, как обычно, и закончить, аннотируя класс @Form. Этот класс @Form может затем быть внедрением параметра в вызов метода любой другой службы. http://docs.jboss.org/resteasy/docs/2.3.5.Final/userguide/html/_Form.html

2 голосов
/ 24 января 2012

Вы можете добавить собственного провайдера, в частности через AbstractHttpContextInjectable:

// FooService.java
@Path("/{id}/foo")
public class FooService
{
    @Context CommonStuff common;

    @GET @Path("bar")
    public Response getBar() { /* snip */ }

    @GET @Path("baz")
    public Response getBaz() { /* snip */ }
}


@Provider
public class CommonStuffProvider
    extends AbstractHttpContextInjectable<CommonStuff>
    implements InjectableProvider<Context, Type>
{

    ...

    @Override
    public CommonStuff getValue(HttpContext context)
    {
        CommonStuff c = new CommonStuff();
        c.id = ...initialize from context;
        c.xyz = ...initialize from context;

        return c;
    }
}

Конечно, вам придется извлекать параметры пути и / или параметры запроса трудным способом из HttpContext, но высделать это один раз в одном месте.

1 голос
/ 22 июня 2016

Вы можете попробовать @BeanParam для всех повторяющихся параметров. так что вместо того, чтобы вводить их каждый раз, вы можете просто ввести свой customBean, который сделает свое дело.

Еще один более чистый подход заключается в том, что вы можете вводить

@Context UriInfo 

или

@Context ExtendedUriInfo

к вашему классу ресурсов, и в самом методе вы можете просто получить к ним доступ. UriInfo более гибок, потому что у вашего jvm будет меньше на один исходный файл java для управления, и, прежде всего, один экземпляр UriInfo или ExtendedUriInfo дает вам множество вещей.

@Path("test")
public class DummyClass{

@Context UriInfo info;

@GET
@Path("/{id}")
public Response getSomeResponse(){
     //custom code
     //use info to fetch any query, header, matrix, path params
     //return response object
}
1 голос
/ 04 мая 2011

Какова мотивация избегать инъекций параметров?
Если мотивация избегает повторения жестко закодированных строк, и вы можете легко переименовать их, вы можете повторно использовать «константы»:

// FooService.java
@Path("/" +  FooService.ID +"/foo")
public class FooService
{
    public static final String ID = "id";
    public static final String XYZ= "xyz";
    public static final String BAR= "bar";

    @PathParam(ID) String id;
    @QueryParam(XYZ) String xyz;

    @GET @Path(BAR)
    public Response getBar() { /* snip */ }

    @GET @Path(BAR)
    public Response getBaz() { /* snip */ }
}

// QuuxService.java
@Path("/" +  FooService.ID +"/quux")
public class QuxxService
{
    @PathParam(FooService.ID) String id;
    @QueryParam(FooService.XYZ) String xyz;

    @GET @Path("abc")
    public Response getAbc() { /* snip */ }

    @GET @Path("def")
    public Response getDef() { /* snip */ }
}

(извините за публикацию второго ответа, но было слишком долго помещать его в комментарии к предыдущему ответу)

0 голосов
/ 04 мая 2011

Вместо использования @PathParam, @QueryParam или любого другого параметра вы можете использовать @Context UriInfo для доступа к любым типам параметров.Таким образом, ваш код может быть:

// FooService.java
@Path("/{id}/foo")
public class FooService
{
    @Context UriInfo uriInfo;

    public static String getIdParameter(UriInfo uriInfo) {
        return uriInfo.getPathParameters().getFirst("id");
    }

    @GET @Path("bar")
    public Response getBar() { /* snip */ }

    @GET @Path("baz")
    public Response getBaz() { /* snip */ }
}

// QuuxService.java
@Path("/{id}/quux")
public class QuxxService
{
    @Context UriInfo uriInfo;

    @GET @Path("abc")
    public Response getAbc() { /* snip */ }

    @GET @Path("def")
    public Response getDef() { /* snip */ }
}

Обратите внимание, что getIdParameter является статическим, поэтому вы можете поместить его в некоторый служебный класс и повторно использовать в нескольких классах.
UriInfo гарантированно безопасен для потоков, поэтомуВы можете сохранить класс ресурсов как синглтон.

...