使用JAX-RS保持DRY原则

64

我正在尝试最小化一些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

资源fooquux之间的共同部分是@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(我也需要),一种可行的方法是使用一个抽象基类,FooServiceQuuxService可以扩展

// 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注入不起作用; 在FooServiceQuuxServiceget*方法中,idxyznull

是否有解决此问题的方法或解决方法?

鉴于CDI按照我的要求工作,我想知道将@PathParam等注入到子类中失败是一个错误还是JAX-RS规范的一部分。
另一种我已经尝试过的方法是使用BaseService作为一个单一入口点,根据需要委托给FooServiceQuuxService。这基本上就像RESTful Java with 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注入无法工作,所以这种丑陋会继续下去。

简而言之,如何避免在JAX-RS资源处理程序类中重复注入字段?

为什么JAX-RS不会注入继承的字段,而CDI却没有这个问题?


编辑 1

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

为什么继承的字段没有被JAX-RS注入?

JSR-311 §3.6中:

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

我相信这个决定有一个真正的原因,但不幸的是,在这个特定的用例中,这个事实对我产生了负面影响。我仍然对任何可能的解决方法感兴趣。


1 使用字段级注入的一个注意事项是我现在与每个请求相关联的资源类实例化绑定,但我可以接受这种情况。
2 因为调用new FooService()的是我自己而不是容器/ JAX-RS 实现。


好问题。不过我不确定你的第一个限制条件是否必要——至少在RESTeasy中,我们已经能够通过RESTeasy的线程本地代理将每个请求的字段级注入用于单例。可能Jersey也是这样做的? - Matthew Gilliard
无论如何,在这一点上,#1 真的不重要。 - Matt Ball
4
“[JSR-311 §3.6]决策的真正原因”似乎是为了避免定义覆盖行为而搪塞:资源绝对应该能够通过Java继承模式来构建。” - rektide
8个回答

7

我正在使用以下解决方法:

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

在所有子类上使用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 */ }
}

4

看起来有人在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 */ }
}

但是看到你这么敏感,坦白地说,我怀疑你的沮丧不会因为这段丑陋的代码而消失 :)


我认为第一个建议不会奏效,因为根据JSR-311 §3.6规定,基本上在子类上放置任何注释都会导致忽略超类上的所有注释。 - Matt Ball
1
这个限制只适用于方法和参数,而不是类。 - Gepsens
我不这么认为,而且我的第二次尝试(在问题中)也证实了这一点。此外,JSR-311 §3.6的直接引用:“如果子类或实现方法具有任何JAX-RS注释,则忽略超类或接口方法上的所有注释。” - Matt Ball
或者如果您嵌套类,会怎样呢?如果它仍然按照文档的引用工作,您可以尝试从存储库获取最新版本,看看他们是否已经开始添加@Path和@Provided的继承(不幸的是,我怀疑这一点)。 - Gepsens
有多种方式可以理解这个引用,我认为有两个范围,即方法和字段+类。你的代码不起作用是因为你在子类上有一个@Path,它取消了字段上的注释。但这并不意味着在方法上有注释就会实际上取消主类上的注释。 - Gepsens
@MattBall 今天刚遇到这个问题,我同意Gepsens对规范的解读。我认为它的意思是:“如果[子类或实现方法]有任何JAX-RS注释,则忽略[超类或接口方法]上的所有注释。” 我认为这个想法是一个方法使用的所有注释应该在一个地方指定,而不是一半在一个地方,另一半在另一个地方。 - Joshua Taylor

3

3
我一直觉得注解继承使得我的代码难以阅读,因为它并不明显地显示注解是从哪里/如何注入的(例如,在继承树的哪个级别注入,并且是否被覆盖)。此外,您必须将变量设置为受保护状态(可能不是最终状态),这会导致超类泄漏其内部状态并可能引入一些错误(至少当我调用扩展方法时,我总会问自己:受保护变量是否在其中更改?)。在我看来,这与DRY无关,因为这不是逻辑封装,而是注入封装,这似乎有点夸张。
最后,我将引用JAX-RS规范中的“3.6注解继承”:
为了与其他Java EE规范保持一致,建议始终重复注解而不是依赖注解继承。
PS:我承认我仅在方法级别上有时使用注解继承 :)

四年前我提出这个问题,现在我并不反对你的观点。但这并不是这里问题的答案。 - Matt Ball
好的,你的问题在谷歌上“jaxrs inheritance”搜索结果的第一页,现在改进还不算晚。虽然这不是你问题的答案,但我希望向大家展示一个更好的选择。 - V G

2

您可以通过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
这是一个特定于Jersey的实现,是还是不是? - rektide

2

避免参数注入的动机是什么?
如果动机是避免重复的硬编码字符串,那么可以重用“常量”来轻松更改它们的名称:

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

很抱歉我要再次回答,但是我的回答太长了,无法放在上一个回答的评论中。


1
非常准确。该部分根本没有提到继承字段。但是第3.6节说:“如果子类或实现方法具有任何JAX-RS注释,则忽略超类或接口方法上的所有注释。”这可能是我尝试的第二种方法存在问题的原因。 - Matt Ball

1
您可以尝试使用@BeanParam来处理所有重复的参数。这样,您就不必每次注入它们,只需注入自定义Bean即可完成操作。
另一种更清晰的方法是,您可以注入。
@Context UriInfo 

or

@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
}

UriInfo 可以再次用于获取任何参数:String queryParam = info.getQueryParameters().getFirst("paramName");文档链接 UriInfo - Najeeb Arif

1

不必使用@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保证是线程安全的,因此您可以将资源类保持为单例。

使用UriInfo确实可以减少注入,但是对于每个类有M个方法,N个类和K个参数(无论该方法位于哪个类中,都是相同的参数),仍然需要程序员(即我)手动提取NKM个参数。 - Matt Ball
无论哪种方式,这两种方法都比将字段级别的注入复制并粘贴到每个类中要多写更多的代码。而不是NKM重复的代码,现在只有N*K重复的代码,因为每个方法可以使用这些字段而不必(显式地)对每个方法进行任何额外的工作。你明白我的意思吗? - Matt Ball

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接