Monday, February 16, 2009

@Resource injection with Jersey

One really annoying thing you find when playing with Jersey is that a lot of the standard annotations you are used to in JEE don't work. This is because Jersey is not entirely plumbed in to the container until it becomes a native service when JEE6 comes along.

It is possible to work around this though by creating your own InjectableProvider that is created by your own version of ServletAdapter. The code looks something like this:

package project1;

import com.sun.jersey.api.container.ContainerException;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.api.core.ResourceConfig;
import com.sun.jersey.spi.container.WebApplication;
import com.sun.jersey.spi.container.servlet.ServletContainer;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
import com.sun.jersey.core.spi.component.ComponentContext;

import com.sun.jersey.core.spi.component.ComponentScope;

import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;

import java.lang.reflect.Proxy;
import java.lang.reflect.Type;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import javax.naming.Context;
import javax.naming.InitialContext;

import javax.naming.NamingException;

import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;

import javax.servlet.ServletConfig;

import javax.ws.rs.core.UriInfo;

import javax.xml.ws.Holder;


public class ServletAdapter
  extends ServletContainer
{
  @Override
  protected void configure(ServletConfig servletConfig, ResourceConfig rc,
                           WebApplication wa)
  {
    super.configure(servletConfig, rc, wa);


    rc.getSingletons().add(new InjectableProvider<Resource, Type>()
      {

        public ComponentScope getScope()
        {
          return ComponentScope.Singleton;
        }

        public Injectable<Object> getInjectable(ComponentContext ic,
                                                Resource r, Type c)
        {

          final Holder value = new Holder();

          try
          {
            Context ctx = new InitialContext();

            // Look up a data source
            try
            {
              value.value = ctx.lookup(r.name());
            }
            catch (NamingException ex)
            {

              value.value = ctx.lookup("java:comp/env/" + r.name());
            }

          }
          catch (Exception ex)
          {
            ex.printStackTrace();
          }

          return new Injectable<Object>()
          {

            public Object getValue()
            {
              return value.value;
            }
          };
        }
      });
  }
}

You then need to make the equivalent changes to your web.xml to make sure that your adapter is used instead of one of the Jersey ones.

<web-app>
    <servlet>
        <display-name>JAX-RS Servlet</display-name>
        <servlet-name>jersey</servlet-name>
        <servlet-class>project1.ServletAdapter</servlet-class>  
        <init-param>
            <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name>
            <param-value>com.sun.jersey.api.core.PackagesResourceConfig</param-value>
        </init-param>
        <init-param>
            <param-name>com.sun.jersey.config.property.packages</param-name>
            <param-value>project1</param-value>
        </init-param>
    </servlet>
</web-app>

Now you get constructor, and field injection on classes that are directly managed by Jersey. In the bug feed application this is only going to be the top level BugFeedDB class. Jersey will inject parameters into the methods that return new resources; but the annoying thing is that @Resource cannot be added onto method parameters.

The two options here are either to pass the value injected @Resource from the root down through the resource chain through the constructors or make up your own marker annotation that can be attached anywhere. The second will probably give you tidier code.

Another quite different way to attack this problem is if you return a class rather than an instance of the class from a resource you will find that injection now works as Jersey can manage the instance. The problem is that you don't have any values from parent resources. In the bug feed case one of the values you need at the tip resource is going to be the user name captured in UserResource.

I tried a few variations and the one I settled on was to have an annotation that you could use to inject previously matched resources in the chain. So I created a marker annotation:

@Retention(RetentionPolicy.RUNTIME)
public @interface PreviousResource {
    
    
}

And then another injectable provider, note the scope here has changed from Singleton to PerRequest so that the injected value updates properly. Note that the injectable extends AbstractHttpContextInjectable so that it can get hold of HttpContext object.


    rc.getSingletons().add(new InjectableProvider<PreviousResource, Type>()
      {

        public ComponentScope getScope()
        {
          return ComponentScope.PerRequest;
        }

        public Injectable<Object> getInjectable(ComponentContext ic,
                                                final PreviousResource r,
                                                final Type t)
        {

          return new AbstractHttpContextInjectable<Object>()
          {

            public Object getValue(HttpContext c)
            {

              List<Object> matched = c.getUriInfo().getMatchedResources();

              for (Object o: matched)
              {
                if (o.getClass() == t)
                {
                  return o;
                }
              }

              return null;
            }
          };
        }
      });

Then you can have it injected where you need it, for example here is the method to return a new resource:

@GET
@Produces("application/atom+xml")
public Feed getBugsAtom(@javax.ws.rs.core.Context
    UriInfo info, @QueryParam(value = "days")
    @DefaultValue("10")
    int days,
    @PreviousResource UserResource ur) {

  String user = ur.getUser();
  ...

}

Other options would be to write a injector that got you previously matched parameters in the URI; but that is left as an exercise to the reader. Not entirely sure how useful @PreviousResource would be in the long term; but it seemed interesting enough to write up. @Resource is definitely worth having working.

3 comments:

Christopher Piggott said...

Thanks for posting this. It works nicely.

I did basically the same thing you did, though I changed getInjectable to return an Injectable<DataSource> instead of a generic object. This means that I also changed Holder to Holder<DataSource>.

Questions:

1) Do you think I'm making a mistake making this handle each class (e.g. DataSource) separately? I was trying to avoid typecasts, but since ctx.lookup() returns Object, there's a typecast anyway

2) About the lifecycle ... from testing it seems that the resource gets injected after the Resource object's constructor is called. Does that make sense? (I hope this isn't a dumb question - Injection is still a bit of a mystery to me).

--Chris

Gerard Davison said...

Hey no problem,

In answer to your questions:

1.

I don't see a problem with your changes; but @Resource can also return web service clients so you would need a version for each. This is why I went with Object all around.

2.

Yup in Jersey things are injected after the constructors are called. Some dependency injection frameworks will do bytecode modification so they can do this before... but the jersey framework just does the basics.

I guess if you need the value during contruction jersey will happily inject into the constructor.

Hope this is of some help,

Gerard

Unknown said...

Hi, I've modified the code a little to accepted field injection by fieldName and also by resource name


try {
Context ctx = new InitialContext();
Field field = (Field) context.getAccesibleObject();
field.getName();
// value.value = ctx.lookup("java:comp/env/" + resource.name());
String lookupName = (resource.name() == null || resource.name()
.isEmpty()) ? field.getName() : resource.name();

try {
value.value = ctx.lookup(lookupName);
} catch (NamingException ex) {
value.value = ctx.lookup("java:comp/env/" + lookupName);
}

} catch (Exception ex) {
ex.printStackTrace();
}