Sunday, January 19, 2014

Bridging Netty, RestEasy and Weld

As you likely know, RestEasy already supports an embedded container for Netty.  RestEasy also supports CDI injection, but only for enterprise use cases (e.g. part of the Java EE spec or using Weld Servlet).  In the case of Netty, it's almost possible, except that the lack of a ServletContext seems to throw it off.

In addition, in many use cases you may want to translate each incoming request into a CDI RequestScope.  This requires some custom handling of each request, before passing it down to RestEasy for processing.  This allows you to properly scope all of your objects, though you cannot use a session scoped object (since there would be no active session).

The code is pretty simple to do this.  You can find details on my github repository: https://github.com/johnament/resteasy-netty-cdi

First, define your endpoint.  In my test case, I added a very simple one:

@Path("/")
@RequestScoped
public class TestEndpoint {
    @GET
    public String echo() {
        return "pong";
    }
}

Next, we need some code to initialize the server.  I added this directly in my test, but I would imagine most people would want to initialize it elsewhere.

CDINettyJaxrsServer netty = new CDINettyJaxrsServer();
        ResteasyDeployment rd = new ResteasyDeployment();
        rd.setActualResourceClasses(paths.getResources());
        rd.setInjectorFactoryClass(CdiInjectorFactory.class.getName());
        netty.setDeployment(rd);
        netty.setPort(8087);
        netty.setRootResourcePath("");
        netty.setSecurityDomain(null);
        netty.start();

As you can see in the test, I am using a custom CdiNettyJaxrsServer, which is what enables me for CDI integration.  The only thing different about mine versus the normal one is what RequestDispatcher I use.  The RequestDispatcher is what RestEasy provides to handle the incoming requests and what the response looks like.  It's very low level.  I decided this was the exact point I wanted to start the CDI RequestScope.  So my RequestDispatcher looks like this

public class CDIRequestDispatcher extends RequestDispatcher
{
    public CDIRequestDispatcher(SynchronousDispatcher dispatcher, ResteasyProviderFactory providerFactory,
                                SecurityDomain domain) {
        super(dispatcher,providerFactory,domain);
    }
    public void service(HttpRequest request, HttpResponse response, boolean handleNotFound) throws IOException
    {
        BoundRequestContext requestContext = CDI.current().select(BoundRequestContext.class).get();
        Map<String,Object> requestMap = new HashMap<String,Object>();
        requestContext.associate(requestMap);
        requestContext.activate();
        try {
            super.service(request,response,handleNotFound);
        }
        finally {
            requestContext.invalidate();
            requestContext.deactivate();
            requestContext.dissociate(requestMap);
        }
    }
}

So whenever a request comes in, I start the context (using Weld's BoundRequestContext) and on completion I end it.  I also created a custom CdiInjectorFactory for Netty.  This alleviates a bug in the base one that depends on a ServletContext being available (throughs a NullPointerException).  It's just a simplified version of the injector factory

    protected BeanManager lookupBeanManager()
    {
        BeanManager beanManager = null;
        beanManager = lookupBeanManagerCDIUtil();
        if(beanManager != null)
        {
            log.debug("Found BeanManager via CDI Util");
            return beanManager;
        }
        throw new RuntimeException("Unable to lookup BeanManager.");
    }

You'll also notice in my test code I'm using a CDI Extension - LoadPathsExtension.  This simply sits on the classpath and listens as Weld initializes.

LoadPathsExtension paths = CDI.current().select(LoadPathsExtension.class).get();

For each ProcessAnnotatedType it observes, it checks if Path is present.  If Path is present, it adds it to a local list of all resources.

public void checkForPath(@Observes ProcessAnnotatedType pat) {
        if(pat.getAnnotatedType().isAnnotationPresent(Path.class)) {
            logger.info("Discovered resource "+pat.getAnnotatedType().getJavaClass());
            resources.add(pat.getAnnotatedType().getJavaClass());
        }
    }

This makes scanning for Paths possible, which is done by the container for RestEasy.  In the Netty deployment, you need to always maintain your list of resources.

LoadPathsExtension paths = CDI.current().select(LoadPathsExtension.class).get();
        CDINettyJaxrsServer netty = new CDINettyJaxrsServer();
        ResteasyDeployment rd = new ResteasyDeployment();
        rd.setActualResourceClasses(paths.getResources());

Finally, we start the actual test which uses the JAX-RS client API to make a request to a specific resource.

        Client c = ClientBuilder.newClient();
        String result = c.target("http://localhost:8087").path("/").request("text/plain").accept("text/plain").get(String.class);
        Assert.assertEquals("pong", result);