About Me

My photo
Related, I keep most of my code here https://github.com/rdammkoehler

Wednesday, December 1, 2010

Guice, Guice Servlet, Jersey, and the Request Scoped Dependancy

On my current project I've been working with Matt Foy to improve the tests around a rather large application. The original developers chose to use Guice with Guice Servlet and Jersey services to create their application. One of the things that we ran into this week was that we had no tests around the Guice configuration. So while everything seemed OK on the CI server, as soon as we actually deployed the application we discovered that the Jersey servlets would not function because various Request Scoped dependancies had not actually been injected. Basically our Module configurations were a mess and we didn't know it.

So, we created a simple sandbox representation of this problem and then discovered a way to effectively test that the Guice configuration was correct. We must give credit to the authors of Guice for making their test available, this is how we figured it out.

First let us show you the classes that I want to test, note this is highly contrived for the purpose of clarity;

The following servlet is simply in place to allow us to have a servlet with a Request Scoped dependency in a Jersey service.
package jerseyandguice;
import javax.servlet.http.HttpServlet;
import javax.ws.rs.Path;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Path("/jg")
@Singleton
public class BogoServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private Dependancy dependancy;
@Inject
public BogoServlet(Dependancy dep) {
dependancy = dep;
}
public Dependancy getDependancy() {
return dependancy;
}
}

The Dependancy is simply an implementation of an interface, neither of which contain any methods;
package jerseyandguice;
public class DefaultDependancy implements Dependancy {
}

And we have a custom module for creating the Dependency. Note that the Dependancy.class is scoped as @RequestScoped;
package jerseyandguice;
import com.google.inject.AbstractModule;
import com.google.inject.servlet.RequestScoped;
public class DependancyModule extends AbstractModule {
@Override
protected void configure() {
bind(Dependancy.class).to(DefaultDependancy.class).in(RequestScoped.class);
}
}

And finally, the GuiceServletContextListener that would be used by the Servlet container to do the actual injections at runtime;
package jerseyandguice;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
import com.google.inject.servlet.ServletModule;
public class JGServletContextListener extends GuiceServletContextListener {
private final ServletModule customServletModule = new ServletModule() {
@Override
protected void configureServlets() {
serve("/jg/*").with(BogoServlet.class);
}
};
@Override
protected Injector getInjector() {
return Guice.createInjector(new DependancyModule(), customServletModule);
}
}

Now that you have the context, here's how we solved the problem. The GuiceFilter used by Guice Servlet support conducts the injection activity for the servlet when a request is executed. If you attempt to get, from the injector, any injected instance that is RequestScoped you will get an exception from the Injector indicating that you've requested a resource outside the scope of a Request. It's your own fault really :-).

So, by contriving a filter chain of assertions you can determine if Guice has actually injected the dependencies correctly. Here's how;

First we created this Assertion class;
package jerseyandguice;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.jmock.Expectations;
import org.jmock.Mockery;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceFilter;
public class GuicedServletAssertion {
public static void assertProvisioningType(final Injector injector,
final Class<?> requestedType, final Class<?> expectedImplementation)
throws Exception {
applyFilterChainAssertion(new FilterChain() {
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse) {
assertTrue(requestedType
.isInstance(injector.getInstance(requestedType)));
assertTrue(expectedImplementation.isInstance(injector
.getInstance(requestedType)));
}
});
}
public static void assertInstantiablity(final Injector injector,
final Class<?> clazz) throws Exception {
applyFilterChainAssertion(new FilterChain() {
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse) {
assertNotNull(injector.getInstance(clazz));
}
});
}
public static void applyFilterChainAssertion(FilterChain filterChain)
throws Exception {
Mockery context = new Mockery();
GuiceFilter filter = new GuiceFilter();
final HttpServletRequest request = context.mock(HttpServletRequest.class);
context.checking(new Expectations() {
{
allowing(request).getAttribute(with(any(String.class)));
will(returnValue(null));
allowing(request).setAttribute(with(any(String.class)),
with(any(Class.class)));
oneOf(request).getServletPath();
}
});
filter.doFilter(request, null, filterChain);
}
public static <T> T expose(final Injector injector, final Class<?> clazz)
throws Exception {
final AtomicReference<T> ref = new AtomicReference<T>();
applyFilterChainAssertion(new FilterChain() {
@SuppressWarnings("unchecked")
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse) {
ref.set((T) injector.getInstance(clazz));
}
});
return ref.get();
}
}

The assertProvisioningType method allows you to assert that an instance is provided for any type request and that the provided instance is of the appropriate type.

The assertInstantiablity method allows you to assert that an instance is provided for a requested type.

The applyFilterChainAssertion method executes the given FilterChain using the GuiceFilter as the head of the chain. JMock is used in this case to support our current project configuration (Note: We'll be rewriting this with Mockito soon enough.) When you call this method with a FilterChain full of assertions they will have access to the Injector and will be within the Request scope, so you can easily make assertions about them without getting the nasty exception pictured here;
1) Error in custom provider, com.google.inject.OutOfScopeException: Cannot access scoped object. Either we are not currently inside an HTTP Servlet request, or you may have forgotten to apply com.google.inject.servlet.GuiceFilter as a servlet filter for this request.
while locating jerseyandguice.Dependancy
1 error
at com.google.inject.InjectorImpl$4.get(InjectorImpl.java:767)
at com.google.inject.InjectorImpl.getInstance(InjectorImpl.java:793)
at jerseyandguice.GuiceWiringTest.shouldFailToGetDependancyBecauseOfScope(GuiceWiringTest.java:62)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:73)
at org.testng.internal.Invoker.invokeMethod(Invoker.java:667)
at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:840)
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1141)
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:135)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:119)
at org.testng.TestRunner.runWorkers(TestRunner.java:1108)
at org.testng.TestRunner.privateRun(TestRunner.java:737)
at org.testng.TestRunner.run(TestRunner.java:596)
at org.testng.SuiteRunner.runTest(SuiteRunner.java:315)
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:310)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:272)
at org.testng.SuiteRunner.run(SuiteRunner.java:221)
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
at org.testng.TestNG.runSuitesSequentially(TestNG.java:946)
at org.testng.TestNG.runSuitesLocally(TestNG.java:883)
at org.testng.TestNG.run(TestNG.java:814)
at org.testng.remote.RemoteTestNG.run(RemoteTestNG.java:89)
at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:144)
Caused by: com.google.inject.OutOfScopeException: Cannot access scoped object. Either we are not currently inside an HTTP Servlet request, or you may have forgotten to apply com.google.inject.servlet.GuiceFilter as a servlet filter for this request.
at com.google.inject.servlet.GuiceFilter.getContext(GuiceFilter.java:132)
at com.google.inject.servlet.GuiceFilter.getRequest(GuiceFilter.java:118)
at com.google.inject.servlet.ServletScopes$1$1.get(ServletScopes.java:48)
at com.google.inject.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:48)
at com.google.inject.InjectorImpl$4$1.call(InjectorImpl.java:758)
at com.google.inject.InjectorImpl.callInContext(InjectorImpl.java:804)
at com.google.inject.InjectorImpl$4.get(InjectorImpl.java:754)
view raw gistfile1.java hosted with ❤ by GitHub

The expose method allows you to directly fetch a Request Scoped instance from the injector for additional testing.

Here is the test suite from our contrived example;
package jerseyandguice;
import static jerseyandguice.GuicedServletAssertion.assertInstantiablity;
import static jerseyandguice.GuicedServletAssertion.assertProvisioningType;
import static jerseyandguice.GuicedServletAssertion.expose;
import static org.testng.Assert.assertNotNull;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.google.inject.Injector;
import com.google.inject.ProvisionException;
public class GuiceWiringTest {
private Injector injector;
@BeforeClass
public void guicify() {
injector = new JGServletContextListener().getInjector();
}
@Test
public void shouldCreateBogoServlet() throws Exception {
assertInstantiablity(injector, BogoServlet.class);
}
@Test(expectedExceptions=ProvisionException.class)
public void shouldFailToCreateDependancyBecauseOfScope() {
injector.getInstance(Dependancy.class);
}
@Test
public void shouldCreateDependancy() throws Exception {
assertInstantiablity(injector, Dependancy.class);
}
@Test
public void shouldProvideDefaultDependancyForDependancy() throws Exception {
assertProvisioningType(injector, Dependancy.class, DefaultDependancy.class);
}
@Test
public void shouldExposeBogoServlet() throws Exception {
BogoServlet bogoServlet = expose(injector, BogoServlet.class);
assertNotNull(bogoServlet);
assertNotNull(bogoServlet.getDependancy());
}
@Test
public void shouldWireDependancyIntoBogoServlet() {
BogoServlet bogoServlet = (BogoServlet) injector.getInstance(BogoServlet.class);
assertNotNull(bogoServlet.getDependancy());
}
@Test
public void shouldProvideDependancy() {
assertNotNull(injector.getProvider(Dependancy.class));
}
@Test
public void shouldProvideBogoServlet() {
assertNotNull(injector.getProvider(BogoServlet.class));
}
}

Hope this helps someone.


No comments: