CDI Injection not possible in a JAX-RS subresource

2019-03-27 21:34发布

问题:

I have a JAX-RS resource class that provides path routing to sub resource classes using @Context ResourceContext to create sub resource instances for each resource type. In this example I am instantiating a reporting sub resource.

Resource

@Context
ResourceContext rc;

@Path("reports")
public ReportsResource reportsResource() {
    return rc.initResource(new ReportsResource());
}

The sub resource needs an instance of a ReportService class (defined with the @Stateless annotation), the natural solution would be to @Inject it ...

Report SubResource

@Inject
ReportsService rs;

@GET
@Path("{rptno}")
@Produces(MediaType.APPLICATION_XML)
public Report report(@PathParam("rptno") int rptNumber) throws Exception {
    return rs.getReport(rptNumber);
}

My experience using Java EE7 with both Glassfish and WAS Liberty Profile is that an instance of ReportService rs is not injected, leaving rs as null and causing a NPE.

My assumption is that because the resource class is doing a "new ReportsResource()", CDI has no visibility to the ReportsResource instance and so ReportsResource is not container managed. This seems to be the same situation as this question Inject EJB into JAX-RS 2.0 subresource when subresource is got via ResourceContext

My solution is somewhat different, I chose to @Inject ReportService in the Resource class, then pass the instance on the ReportsResource constructor.

Modified Resource

@Inject
ReportsSerivce rs;

@Context
ResourceContext rc;

@Path("reports")
public ReportsResource reportsResource() {
    return rc.initResource(new ReportsResource(rs));
}

Modified Report Subresource

public class ReportsResource {
    private ReportsSerivce rs;

    public ReportsResource(ReportsSerivce rs) {
      this.rs = rs;
    }

    @Context
    HttpHeaders headers;

    @GET
    @Path("{rptno}")
    @Produces(MediaType.APPLICATION_XML)
    public Report report(@PathParam("rptno") int rptNumber) throws Exception {
        return rs.getReport(rptNumber);
    }

So to my questions

  1. Is my assumption about why @Inject fails correct?
  2. Is there any way to make @Inject work in the sub resource?
  3. Is there a better solution to passing the ReportService instance from Resource to SubResource that is more "CDI/Java EE" like?

回答1:

If you want to inject CDI beans into JAX-RS resources, I don't recommend using rc.initResource. All it does is injection of fields into existing object, but it uses JAX-RS specific mechanism, similar to how injection worked for EJBs in JavaEE5, when CDI was not available.

It is better to use CDI and remove ResourceContext from your code.

Example:

Resource

@Inject
private ReportsResource reportsResource;

@Path("reports")
public ReportsResource reportsResource() {
    return reportsResource;
}

The master resource should be @RequestScoped so that it gets recreated for each request. Or you may inject using Intance to get new instance for every method call:

@Inject
private Instance<ReportsResource> reportsResources;

@Path("reports")
public ReportsResource reportsResource() {
    return reportsResources.get();
}


回答2:

Note that injecting the sub resource directly into the root resource may cause a problem. I'm sharing what I've learnt.

@Path("parents")
class ParentsResource {

    @Path("/{parentId: \\d+}/{children: children}");
    public ChildrenResource resourceChildren() {
        return childrenResource;
    }

    @Inject
    private ChildrenResource childrenResource;
}

class ChildrenResource {

    @PostConstruct
    private void onPostConstruct() {
        parentName = children.getMatrixParameters().getFirst("parentName");
    }

    @PathParam("children");
    private PathSegment children; // may or may not be null

    private String parentName;
}

Following works.

/parents/1/children
/parents/1/children;parentName=Kwon

And we might got a NullPointerException when we simple call

/parents
/parents/1

Because the injection of the ChildrenResource instance itself happens before it injected into the ParentResource.

In this case, Optional might help

parentName = ofNullable(children)
        .map(v -> v.getMatrixParameters().getFirst("parentName")
        .orElse(null);

But using ResourceContext can be said more proper.



回答3:

According to the documentation of Jersey (JAX-RS reference implementation) if you want your sub-resource's lifecycle to be managed by container, you have to return the class type and not an instance of it. You can then manage the lifecycle of sub-resource as you wish like any other container managed resource.

For example:

import javax.inject.Singleton;

@Path("/item")
public class ItemResource {
    @Path("content")
    public Class<ItemContentSingletonResource> getItemContentResource() {
        return ItemContentSingletonResource.class;
    }
}

@Singleton
public class ItemContentSingletonResource {
    // this class is managed in the singleton life cycle
}

more info could be found on docs: https://jersey.java.net/documentation/latest/jaxrs-resources.html#d0e2496



回答4:

This works for me

Resource

@Path("reports")
public ReportsResource reportsResource() {
    return CDI.current().select(ReportsResource.class).get();
}

Subresource

@Inject ReportBean reportBean;

@GET
@Path("/{rptno}")
@Produces(MediaType.APPLICATION_XML)
public Report report(@PathParam("rptno") int rptNumber) throws Exception {
    return reportBean.getReport(rptNumber);
}