【问题标题】:Staying DRY with JAX-RS使用 JAX-RS 保持干燥
【发布时间】:2011-08-18 01:27:25
【问题描述】:

我正在尝试尽量减少许多 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(我也需要)的方法是使用 abstract 基类,FooServiceQuuxService 可以 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 注入不起作用idxyznullFooServiceFooServiceQuuxServiceget* 方法中。

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

鉴于 CDI 按我的意愿工作,我想知道将 @PathParams (等)注入子类失败是错误还是 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注入不行,所以丑继续。


tl;dr 避免跨 JAX-RS 资源处理程序类重复注入字段的正确方法是什么?

为什么 JAX-RS 不注入继承字段,而 CDI 对此没有问题?


编辑 1

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

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

JSR-311 §3.6:

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

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


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

【问题讨论】:

  • 好问题。不过,我不确定您的警告 #1 是否必要——至少使用 RESTeasy,我们已经能够通过 RESTeasy 的线程本地代理将每个请求的字段级注入单例。泽西岛可能也是这样吗?
  • 无论如何,#1 在这一点上真的无关紧要。
  • “[JSR-311 §3.6] 决定的真正原因”似乎是为了避免不得不费心定义覆盖行为:资源绝对应该能够通过 Java 继承模式构建。

标签: java jersey jax-rs


【解决方案1】:

您可以使用@Context UriInfo 来访问任何类型的参数,而不是使用@PathParam@QueryParam 或任何其他参数。所以你的代码可能是:

// 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 个参数,必须由程序员(我)手动提取。
  • 无论哪种方式,这两种方法都比将字段级注入复制并粘贴到每个类中更多的代码。不是 NKM 重复代码,而是 N*K 重复代码,因为每个方法都可以使用字段,而不必(明确地)为每个方法做任何额外的工作。你明白我的意思吗?
【解决方案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 */ }
}

(抱歉发布第二个答案,但是太长了,无法将它放在上一个答案的评论中)

【讨论】:

  • 正确。该部分根本没有提到继承的字段。但是第 3.6 节说 “如果子类或实现方法有任何 JAX-RS 注释,则忽略超类或接口方法上的所有注释。” 这可能是我第二种方法的问题试过了。
【解决方案3】:

看着Jax's 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 - 基本上,将 any 注释放在子类上会导致 所有要忽略的超类注释。
  • 这个限制只针对方法和参数,不针对类?
  • 我不这么认为,我的第二次尝试(在问题中)支持了这一点。此外,直接引用 JSR-311 §3.6:“如果子类或实现方法具有任何 JAX-RS 注释,则忽略超类或接口方法上的所有注释。”
  • 或者如果你嵌套类怎么办?如果它仍然根据文档的报价工作,您可以尝试从存储库获取最新版本,看看他们是否已经开始向 @Path 和 @Provided 添加继承(不幸的是,我怀疑)。
  • 有多种方法可以获取该引用,正如我所见,有两个范围,方法和字段+类,您的代码不起作用,因为您有一个 @Path取消字段上的注释的子类。但这并不意味着在方法上有注解,实际上取消了主类上的注解。
【解决方案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 中提取路径参数和/或查询参数,但您需要在一个地方提取一次。

【讨论】:

  • 这是 Jersey 特定的实现,c/d?
【解决方案5】:

在 RESTEasy 中,可以构造一个类,像往常一样使用 @*Param 注释,最后通过注释类 @Form 来完成。这个@Form 类可能是一个参数注入到任何其他服务的方法调用中。 http://docs.jboss.org/resteasy/docs/2.3.5.Final/userguide/html/_Form.html

【讨论】:

    【解决方案6】:

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

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

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

    【讨论】:

      【解决方案7】:

      我一直有一种感觉,注解继承使我的代码不可读,因为从哪里/如何注入它并不明显(例如,它将在继承树的哪个级别注入以及它在哪里被覆盖(或被它完全被覆盖))。此外,您必须使变量受保护(并且可能不是最终的),这会使超类泄漏其内部状态并且还可能引入一些错误(至少在调用扩展方法时我总是会问自己:受保护的变量是否在那里更改?)。恕我直言,它与 DRY 无关,因为这不是对逻辑的封装,而是对注入的封装,这对我来说似乎有些夸张。

      最后我将引用 JAX-RS 规范,3.6 Annotation Inheritance

      为了与其他 Java EE 规范保持一致,建议 总是重复注释而不是依赖注释 继承。

      PS:我承认我有时只使用注解继承,但在方法级别:)

      【讨论】:

      • 在我提出这个问题四年后,我不同意您的意见。但这不是这里问题的答案。
      • 好吧,你的问题在谷歌的第一页上是关于“jaxrs 继承”的,改进这些东西可能永远不会太晚。同意这不是您问题的答案,但我只是希望向人们展示一个更好的选择。
      【解决方案8】:

      您可以尝试使用 @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
      }
      

      【讨论】:

      • UriInfo 这里可以再次用于获取任何参数: String queryParam = info.getQueryParameters().getFirst("paramName");文档链接UriInfo
      猜你喜欢
      • 1970-01-01
      • 2023-03-08
      • 2014-06-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多