Staying DRY with JAX-RS
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
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 */ }
}
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 r开发者_开发百科esource 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 */ }
}
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
.
Is there a fix or workaround for this problem?
Given that the CDI works as I'd like it to, I'm wondering if the failure to inject @PathParam
s (etc.) into subclasses is a bug or just part of the JAX-RS spec.
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 */ }
}
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.
tl;dr What's the right way to avoid repeatedly injecting fields across JAX-RS resource handler classes?
And why aren't inherited fields injected by JAX-RS, while CDI has no issues with this?
Edit 1
With a bit of direction from @Tarlog, I think I've found the answer to one of my questions,
Why aren't inherited fields injected by JAX-RS?
In JSR-311 §3.6:
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 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 callingnew FooService()
rather than the container/the JAX-RS implementation.Here is a workaround I'm using:
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 */ }
}
Looking at Jax's JIRA it seems someone asked for annotation inheritance as milestone for JAX-RS.
The feature you're looking for just doesn't exist in JAX-RS yet, however, would this work? It's ugly, but prevents recurrent injection.
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 */ }
}
Or in another workaround :
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 */ }
}
But seeing how touchy you are, frankly, I doubt your frustration will go away with this ugly code :)
In RESTEasy one can construct a class, annotate with @*Param as usual, and finish by annotating the class @Form. This @Form class may then be a parameter injection into any other service's method call. http://docs.jboss.org/resteasy/docs/2.3.5.Final/userguide/html/_Form.html
I always had a feeling, that annotation inheritance makes my code unreadable, as it is not obvious from where/how it is injected (e.g on which level of the inheritance tree would it be injected and where was it overriden (or was it overriden at all)). Moreover, you have to make the variable protected (and probably NOT final), which makes the superclass leak its internal state and also may introduce some bugs ( at least I would always ask myself when calling an extended method: is the protected variable changed there?). IMHO it has nothing with DRY, as this is not encapsulation of logic, but encapsulation of injection, which seems exaggerated to me.
At the end I will cite from the JAX-RS spec, 3.6 Annotation Inheritance
For consistency with other Java EE specifications, it is recommended to always repeat annotations instead of relying on annotation inheritance.
PS: I admit that I use only sometimes annotation inheritance, but on the method level :)
What is the motivation of avoiding parameters injections?
If the motivation is avoiding of repeating hard-coded strings, so you can easily rename them, you can reuse "constants":
// 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 */ }
}
(Sorry for posting the second answer, but it was too long to put it in a comment of the previous answer)
You can add a custom provider, particularly via 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;
}
}
Granted, you'll have to extract the path parameters and/or the query parameters the hard way from HttpContext, but you'll do it once in one place.
Instead of using @PathParam
, @QueryParam
or any other param, you can use @Context UriInfo
to access any types of parameters. So your code could be:
// 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 */ }
}
Pay attention that getIdParameter
is static, so you can put it in some utility class and reuse accorss multiple classes.
UriInfo is guaranteed to be threadsafe, so you can keep resource class as singleton.
You can try @BeanParam for all the repeating params. so rather than injecting them every time you can simply inject you customBean which will do the trick.
Another approach which is more cleaner is that you can inject
@Context UriInfo
or
@Context ExtendedUriInfo
to your Resource Class and in very method you can simply access them. UriInfo is more flexible because your jvm will have one less java source file to manage and above all single instance of UriInfo or ExtendedUriInfo gives you a handle of a lot of things.
@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
}
精彩评论