使用 JAX-RS 保持干燥

2022-01-21 00:00:00 java jax-rs jersey

我正在尝试尽量减少许多 JAX-RS 资源处理程序的重复代码,所有这些都需要一些相同的路径和查询参数.每个资源的基本 url 模板如下所示:

I'm trying to minimize repeated code for a number of JAX-RS resource handlers, all of which require a few of the same path and query parameters. The basic url template for each resource looks like this:

/{id}/resourceName

并且每个资源都有多个子资源:

and each resource has multiple subresources:

/{id}/resourceName/subresourceName

因此,资源/子资源路径(包括查询参数)可能看起来像

So, resource/subresource paths (incl. query parameters) might look like

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

资源 fooquux 的共同部分是 @PathParam("id")@QueryParam(";xyz").我可以像这样实现资源类:

The common parts across resources foo and quux are @PathParam("id") and @QueryParam("xyz"). I could implement the resource classes like this:

// 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 基类 FooServiceQuuxService 可以 extend:

I've managed to avoid repeating the parameter injection into every single get* method.1 This is a good start, but I'd like to be able to avoid the repetition across resource classes as well. An approach that works with CDI (which I also need) is to use an abstract base class which FooService and QuuxService could 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 注入不起作用.idxyzFooServiceget* 方法中的 null>QuuxService.

Inside of the get* methods, the CDI injection (miraculously) works correctly: the util field is not null. Unfortunately, the JAX-RS injection does not work; id and xyz are null in the get* methods of FooService and QuuxService.

鉴于 CDI 可以按我的意愿工作,我想知道将 @PathParams (等)注入子类的失败是错误还是 JAX 的一部分-RS 规范.

Given that the CDI works as I'd like it to, I'm wondering if the failure to inject @PathParams (etc.) into subclasses is a bug or just part of the JAX-RS spec.

我已经尝试过的另一种方法是使用 BaseService 作为单一入口点,根据需要委托给 FooServiceQuuxService.这基本上如 RESTful Java with JAX-RS 使用子资源定位器.

Another approach I have already tried is using BaseService as a single point of entry that delegates to FooService and QuuxService as needed. This is basically as described in RESTful Java with JAX-RS using subresource locators.

// 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注入不起作用,所以丑的继续.

The downside to this approach is that neither CDI injection nor JAX-RS injection works in the subresource classes. The reason for this is fairly obvious2, but what that means is that I have to manually re-inject the fields into the subclasses' constructor, which is messy, ugly, and doesn't easily let me customize further injection. Example: say I wanted to @Inject an instance into FooService but not QuuxService. Because I'm explicitly instantiating the subclasses of BaseService, CDI injection won't work, so the ugliness is continued.

在 @Tarlog 的指导下,我想我已经找到了我的一个问题的答案,

With a bit of direction from @Tarlog, I think I've found the answer to one of my questions,

为什么 JAX-RS 不注入继承的字段?

Why aren't inherited fields injected by JAX-RS?

在 JSR-311 §3.6:

如果子类或实现方法有任何 JAX-RS 注释,则所有超类或接口方法上的注释将被忽略.

If a subclass or implementation method has any JAX-RS annotations then all of the annotations on the super class or interface method are ignored.

我确信这个决定是有真正的原因的,但不幸的是,在这个特定的用例中,这个事实对我不利.我仍然对任何可能的解决方法感兴趣.

I'm sure that there's a real reason for this decision, but unfortunately that fact is working against me in this particular use case. I'm still interested in any possible workarounds.

1 使用字段级注入的警告是,我现在绑定到每个请求的资源类实例化,但我可以忍受.
2 因为我是调用 new FooService() 而不是容器/JAX-RS 实现的人.

1 The caveat with using field-level injection is that I'm now tied to per-request resource class instantiation, but I can live with that.
2 Because I'm the one calling new FooService() rather than the container/the JAX-RS implementation.

推荐答案

这是我正在使用的解决方法:

Here is a workaround I'm using:

使用 'id' 和 'xyz' 作为参数为 BaseService 定义一个构造函数:

Define a constructor for the BaseService with 'id' and 'xyz' as params:

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

使用注入在所有子类上重复构造函数:

Repeat the constructor on all subclasses with the injects:

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

相关文章