How to integrate JAX-RS with CDI in a Servlet 3.0

2020-02-10 04:58发布

问题:

I have a web application running on a Servlet 3.0 container (Jetty 9.0.4) using JSF 2.2 (Mojorra 2.1.3) & CDI 1.1 (Weld 2.0.3). No full-fledged application server is used. In this application I also have a JAX-RS 2.0 (Jersey 2.2) resource class serving REST requests. I have integrated JAXB binding and also JSON marshalling (Jackson 2.2). I use Maven 3.0.5 for the build management. These are the relevant parts of my project setup:

Maven pom.xml:

...
<dependencies>

    <!-- Servlet 3.0 API -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>

    <!-- Contexts and Dependency Injection for Java EE -->
    <dependency>
        <groupId>org.jboss.weld.servlet</groupId>
        <artifactId>weld-servlet</artifactId>
        <version>2.0.3.Final</version>
    </dependency>

    <!-- JavaServer Faces -->
    <dependency>
        <groupId>com.sun.faces</groupId>
        <artifactId>jsf-api</artifactId>
        <version>2.2.2</version>
    </dependency>
    <dependency>
        <groupId>com.sun.faces</groupId>
        <artifactId>jsf-impl</artifactId>
        <version>2.2.2</version>
    </dependency>

    <!-- JAX-RS RESTful Web Services -->
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet</artifactId>
        <version>2.2</version>
    </dependency>

    <!-- JSON Mapping Framework -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>2.2</version>
    </dependency>
</dependencies>
...

Deployment Descriptor web.xml:

...
<context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
</context-param>

<servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/jsf/*</url-pattern>
</servlet-mapping>

<servlet>
    <servlet-name>Jersey REST Service</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
        <param-name>javax.ws.rs.Application</param-name>
        <param-value>my.package.config.RestApplication</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>Jersey REST Service</servlet-name>
    <url-pattern>/rest/*</url-pattern>
</servlet-mapping>  

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

<listener>
    <listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>

<listener>
    <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>

<resource-env-ref>
    <description>Object factory for the CDI Bean Manager</description>
    <resource-env-ref-name>BeanManager</resource-env-ref-name>
    <resource-env-ref-type>javax.enterprise.inject.spi.BeanManager</resource-env-ref-type>
</resource-env-ref>
...

JAX-RS root resource class:

@Path("/person")
public class PersonController
{
    @Inject
    private PersonService personService;

    @GET
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public List<Person> getAllPersons()
    {
        return personService.getAll();
    }

    @GET
    @Path("/{index}")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Person getPerson(@PathParam("index") int index)
    {
        return personService.get(index);
    }

    @POST
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public void savePerson(Person person)
    {
        personService.add(person);
    }
}

JAX-RS application configuration:

public class RestApplication extends ResourceConfig
{
    public RestApplication()
    {
        // For JSON binding
        register(new JacksonFeature());
        register(new ApplicationBinder());
        packages("my.package.controller");
    }
}

JAX-RS injection binding configuration:

public class ApplicationBinder extends AbstractBinder
{
    @Override
    protected void configure()
    {
        // Means something like: bind the field at an injection point of type PersonService to an instance of type PersonService
        bind(PersonService.class).to(PersonService.class);
    }
}

And finally the JSF managed bean:

@Named
@SessionScoped
public class PersonBean implements Serializable
{
    private static final long serialVersionUID = 1L;

    @Inject
    private PersonService personService;

    private Person newPerson = new Person();

    public List<Person> getAll()
    {
        return personService.getAll();
    }

    public Person getNewPerson()
    {
        return newPerson;
    }

    public void setNewPerson(Person newPerson)
    {
        this.newPerson = newPerson;
    }

    public Gender[] getGenders()
    {
        return Gender.values();
    }

    public String saveNewPerson()
    {
        personService.add(newPerson);
        newPerson = new Person();

        return "index";
    }
}

At the end, I want to be able to use the same application scoped service instances in the REST resource classes as well as in the JSF beans, but I can't get CDI and JAX-RS work together.

The JSF/CDI part works fine, but the injection into the REST resource classes does not really work. I read some articles, where they showed two different approaches to combine CDI and JAX-RS: The first one is to annotate the REST resource class with @ManagedBean in order for the class to be instantiated by the CDI container and to be managed by the JAX-RS container:

@ManagedBean
@Path("/person")
public class PersonController
{
    @Inject
    private PersonService personService;
    ...

The second approach is to give the class a CDI scope, e.g. @RequestScoped in order for the class to be instantiated and managed by the CDI container.

@Path("/person")
@RequestScoped
public class PersonController
{
    @Inject
    private PersonService personService;
    ...

None of the approaches work for me. I always end up with the following exception:

org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available for injection at Injectee(requiredType=PersonService,parent=PersonController,qualifiers={}),position=-1,optional=false,self=false,unqualified=null,5643079)
    at org.jvnet.hk2.internal.ThreeThirtyResolver.resolve(ThreeThirtyResolver.java:74)
    at org.jvnet.hk2.internal.ClazzCreator.resolve(ClazzCreator.java:208)
    at org.jvnet.hk2.internal.ClazzCreator.resolveAllDependencies(ClazzCreator.java:231)
    at org.jvnet.hk2.internal.ClazzCreator.create(ClazzCreator.java:328)
    at org.jvnet.hk2.internal.SystemDescriptor.create(SystemDescriptor.java:454)
    at org.glassfish.jersey.process.internal.RequestScope.findOrCreate(RequestScope.java:158)
    at org.jvnet.hk2.internal.Utilities.createService(Utilities.java:2296)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.getService(ServiceLocatorImpl.java:590)
    at org.jvnet.hk2.internal.ServiceLocatorImpl.getService(ServiceLocatorImpl.java:577)
    at org.glassfish.jersey.internal.inject.Injections.getOrCreate(Injections.java:172)

But this error disappears when changing the injection binding configuration to:

bind(PersonServiceImpl.class).to(PersonService.class);

Now injection somehow works, but for every REST request I get a new instance of the PersonServiceImpl injected, even if this service is application scoped. To me this is an indicator, that the JAX-RS component is totally separated from the CDI stuff and lives in a completely different environment or container as the CDI / JSF stuff does.

So I really wonder how to make these two concepts work together in a pure servlet 3.0 container.

回答1:

I solved my problem.

The problem is, that Jersey JAX-RS implementation uses the HK2 dependency injection framework and this framework is simply not aware of the CDI beans. And by following the idea of the accepted answer in this post, I make the CDI beans available for the HK2 injection bindings and the injection of my application scoped beans works fine now.

But I really wonder why it is so cumbersome to bring together two constituent parts of Java EE.

Update: As G. Demecki mentioned in a comment, this is solution is no longer needed! But it helped me out at the time of asking this question.



回答2:

You can just include this dependency to enable Jersey-CDI integration

<dependency>
    <groupId>org.glassfish.jersey.containers.glassfish</groupId>
    <artifactId>jersey-gf-cdi</artifactId>
    <version>2.2</version>
</dependency>

It works for me using Jersey 2.8, Jetty 9.1 and Weld 2.2.0.SP1.



回答3:

I have created a sample project with Embedded Jetty, Jersey 2 and CDI https://drive.google.com/file/d/0B9j0dIS5bS0wSEg0NTk2eGpPSGs/view?usp=sharing

Kindly let me know if you find any issues