Пребывание DRY с помощью JAX-RS

Я пытаюсь свести к минимуму повторяющийся код для нескольких обработчиков ресурсов 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 не равно null. К сожалению, инъекция JAX-RS не работает; id и xyz являются null в методах get* FooService и QuuxService.

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

Учитывая, что CDI работает так, как мне бы хотелось, мне интересно, является ли ошибка при вводе @PathParam (и т.д.) в подклассы ошибкой или просто частью спецификации 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?

В JSR-311 §3.6:

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

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


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

Ответ 1

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

Определите конструктор для 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 */ }
}

Ответ 2

Глядя на Jax JIRA, похоже, кто-то попросил наследование аннотаций как веху для 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

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

Ответ 4

Вы можете добавить настраиваемого поставщика, особенно через 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, но вы сделаете это один раз в одном месте.

Ответ 5

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

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

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

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

Ответ 6

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

// 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 */ }
}

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

Ответ 7

Вы можете попробовать @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
}

Ответ 8

Вместо использования @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 является статическим, поэтому вы можете поместить его в некоторый класс утилиты и повторно использовать accorss для нескольких классов.
UriInfo гарантированно будет потокобезопасным, поэтому вы можете сохранить класс ресурсов как singleton.