Search code examples
javadependency-injectionjerseyhk2

Jersey/HK2 - injecten of HttpServletRequest inside ContainerRequestFilter via annotated injection


I have an annotation @MagicAnnotation which allows me to inject parameters into my resources. The implementation is as following:

@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MagicAnnotation {
}

public class MagicResolver extends ParamInjectionResolver<MagicAnnotation> {
    public MagicResolver() {
        super(MagicProvider.class);
    }
}

public class MagicProvider extends AbstractValueFactoryProvider {
    @Inject
    public MagicProvider(final MultivaluedParameterExtractorProvider provider, final ServiceLocator locator) {
        super(provider, locator, Parameter.Source.UNKNOWN);
    }

    @Override
    protected Factory<?> createValueFactory(final Parameter parameter) {
        return new MagicFactory();
    }
}

public class MagicFactory extends AbstractContainerRequestValueFactory<String> {
    @Context
    private HttpServletRequest request;

    @Override
    public String provide() {
        return request.getParameter("value");
    }
}

In my JAX-RS configuration, I register the binder as following:

public class MagicBinder extends AbstractBinder {
    @Override
    protected void configure() {
        bind(MagicProvider.class).to(ValueFactoryProvider.class).in(Singleton.class);
        bind(MagicResolver.class).to(new TypeLiteral<InjectionResolver<MagicAnnotation>>() {
        }).in(Singleton.class);
    }
}

register(new MagicBinder());

This works great. An example of usage:

@Path("/magic")
public class SomeTest {
    @MagicAnnotation
    private String magic;

    @GET
    public Response test() {
        return Response.ok(magic).build();
    }
}

Now, I want to use @MagicAnnotation inside a ContainerRequestFilter. I tried as following:

@Provider
public class MagicFilter implements ContainerRequestFilter {
    @MagicAnnotation
    private String magic;

    @Override
    public void filter(final ContainerRequestContext context) {
        if (!"secret".equals(magic)) {
            throw new NotFoundException();
        }
    }
}

This gives the following during initialization:

java.lang.IllegalStateException: Not inside a request scope

After some debugging, I found out that the injection of HttpServletRequest in MagicFactory is the problem. I guess that HttpServletRequest is a request-contextual class (it is different on every HTTP request) and HK2 is unable to create a proxy for that class. Shouldn't HttpServletRequest be already a proxy by itself?

How can I get around this?


Solution

  • Shouldn't HttpServletRequest be already a proxy by itself?

    Yes it is, but because you are trying to inject the magic annotation target into a filter (which is a singleton that gets instantiated at application start), the provide() method of the factory gets called, which calls the HttpServletRequest. And because there is no request on startup, you get the "not in a reuqest scope" error.

    The easiest fix is just to use javax.inject.Provider to lazily retrieve the injected object. This way, the factory isn't called until you request for the object, by calling Provider#get().

    @Provider
    public class MagicFilter implements ContainerRequestFilter {
        @MagicAnnotation
        private Provider<String> magic;
    
        @Override
        public void filter(final ContainerRequestContext context) {
                               // Provider#get()
            if (!"secret".equals(magic.get())) {
                throw new NotFoundException();
            }
        }
    }
    

    UPDATE

    Ok, so the above solution won't work. It seems that even using Provider, the factory is still called.

    What we need to do is make the magic value a proxy. But a String can't be proxied, so I made a wrapper.

    public class MagicWrapper {
        private String value;
    
        /* need to proxy */
        public MagicWrapper() {   
        }
    
        public MagicWrapper(String value) {
            this.value = value;
        }
    
        public String get() {
            return this.value;
        }
    }
    

    Now for some restructuring. First thing we should understand is the required components. The pattern you are currently using for parameter injection is the pattern used in the Jersey source to handle parameter injection for params like @PathParam and @QueryParam.

    The classes used as part of that infrastructure is the AbstractValueFactoryProvider and the ParamInjectionResolver that you are using. But these classes are only really helper classes that Jersey uses to keep it DRY, as there are many different types of params to inject. But those classes are only extension of the main contracts that need to be implemented to handle this use case, namely ValueFactoryProvider and InjectResolver. So we can restructure our use case by directly implementing those contracts, instead of using Jersey's "helper" infrastructure. This allows us to create the proxy where needed.

    To create the proxy for our MagicWrapper, we just configure the Factory for it in the AbstractBinder as a proxy

    @Override
    public void configure() {
        bindFactory(MagicWrapperFactory.class)
            .to(MagicWrapper.class)
            .proxy(true)
            .proxyForSameScope(false)
            .in(RequestScoped.class);
    }
    

    The call to proxy makes the object proxiable, and the call to proxyForSameScope(false) ensures that when it is in a request scope, it is the actual object, instead of a proxy. Doesn't really make much of a difference here. Only really one that matters is the call to proxy().

    Now to handle custom annotation injection, we need an InjectionResolver. That's its job.

    public class MagicInjectionResolver implements InjectionResolver<MagicAnnotation> {
    
        @Inject @Named(InjectionResolver.SYSTEM_RESOLVER_NAME)
        private InjectionResolver<Inject> systemResolver;
    
        @Override
        public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
            if (injectee.getRequiredType() == MagicWrapper.class) {
                return systemResolver.resolve(injectee, handle);
            }
            return null;
        }
    
        @Override
        public boolean isConstructorParameterIndicator() { return false; }
    
        @Override
        public boolean isMethodParameterIndicator() { return true; }
    }
    

    As stated above, the ParamInjectionResolver you are currently using, is just an implementation of InjectionResolver which is more simplified, but won't work for this case. So we just implement it ourselves. We don't really do anything but check the type so that we only handle injections for MagicWrappers. Then we just delegate the work to the system InjectionResolver.

    Now we need the component that Jersey uses for method parameter injection, which is the ValueFactoryProvider.

    public class MagicValueFactoryProvider implements ValueFactoryProvider {
    
        @Inject
        private ServiceLocator locator;
    
        @Override
        public Factory<?> getValueFactory(Parameter parameter) {
            if (parameter.isAnnotationPresent((MagicAnnotation.class))) {
                final MagicWrapperFactory factory = new MagicWrapperFactory();
                locator.inject(factory);
                return factory;
            }
            return null;
        }
    
        @Override
        public PriorityType getPriority() {
            return Priority.NORMAL;
        }
    }
    

    Here we are just returning the factory, just like you did in the AbstractValueFactoryProvider. Only difference is, we need to explicitly inject it, so that it gets the HttpServletRequest. This is the same thing the Jersey does in the AbstractValueFactoryProvider.

    And that's it. Below is a complete example using Jersey Test Framework. Run it like any other JUnit test.

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import java.util.logging.Logger;
    
    import javax.inject.Inject;
    import javax.inject.Named;
    import javax.inject.Singleton;
    import javax.servlet.http.HttpServletRequest;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.container.ContainerRequestContext;
    import javax.ws.rs.container.ContainerResponseContext;
    import javax.ws.rs.container.ContainerResponseFilter;
    import javax.ws.rs.core.Context;
    import javax.ws.rs.core.Response;
    import javax.ws.rs.ext.Provider;
    
    import org.glassfish.hk2.api.Factory;
    import org.glassfish.hk2.api.Injectee;
    import org.glassfish.hk2.api.InjectionResolver;
    import org.glassfish.hk2.api.ServiceHandle;
    import org.glassfish.hk2.api.ServiceLocator;
    import org.glassfish.hk2.api.TypeLiteral;
    import org.glassfish.hk2.utilities.binding.AbstractBinder;
    import org.glassfish.jersey.filter.LoggingFilter;
    import org.glassfish.jersey.process.internal.RequestScoped;
    import org.glassfish.jersey.server.ResourceConfig;
    import org.glassfish.jersey.server.model.Parameter;
    import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider;
    import org.glassfish.jersey.servlet.ServletContainer;
    import org.glassfish.jersey.test.DeploymentContext;
    import org.glassfish.jersey.test.JerseyTest;
    import org.glassfish.jersey.test.ServletDeploymentContext;
    import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
    import org.glassfish.jersey.test.spi.TestContainerFactory;
    import org.junit.Test;
    
    import static junit.framework.Assert.assertEquals;
    
    /**
     * See http://stackoverflow.com/q/39411704/2587435
     * 
     * Run like any other JUnit test. Only one require dependency
     * <dependency>
     *   <groupId>org.glassfish.jersey.test-framework.providers</groupId>
     *   <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
     *   <version>${jersey2.version}</version>
     *   <scope>test</scope>
     * </dependency>
     *
     * @author Paul Samsotha
     */
    public class InjectionTest extends JerseyTest {
    
        @Path("test")
        public static class TestResource {
            @GET
            public String get(@MagicAnnotation MagicWrapper magic) {
                return magic.get();
            }
        }
    
        @Provider
        public static class MagicFilter implements ContainerResponseFilter {
    
            @MagicAnnotation
            private MagicWrapper magic;
    
            @Override
            public void filter(ContainerRequestContext request, ContainerResponseContext response) {
                response.getHeaders().add("X-Magic-Header", magic.get());
            }
        }
    
        @Override
        public ResourceConfig configure() {
            return new ResourceConfig()
                .register(TestResource.class)
                .register(MagicFilter.class)
                .register(new LoggingFilter(Logger.getAnonymousLogger(), true))
                .register(new AbstractBinder() {
                    @Override
                    public void configure() {
                        bindFactory(MagicWrapperFactory.class)
                                .to(MagicWrapper.class)
                                .proxy(true)
                                .proxyForSameScope(false)
                                .in(RequestScoped.class);
                        bind(MagicInjectionResolver.class)
                                .to(new TypeLiteral<InjectionResolver<MagicAnnotation>>(){})
                                .in(Singleton.class);
                        bind(MagicValueFactoryProvider.class)
                                .to(ValueFactoryProvider.class)
                                .in(Singleton.class);
                    }
                });
        }
    
        @Override
        public TestContainerFactory getTestContainerFactory() {
            return new GrizzlyWebTestContainerFactory();
        }
    
        @Override
        public DeploymentContext configureDeployment() {
            return ServletDeploymentContext.forServlet(new ServletContainer(configure())).build();
        }
    
        @Retention(RetentionPolicy.RUNTIME)
        @Target({ElementType.PARAMETER, ElementType.FIELD})
        public static @interface MagicAnnotation {
        }
    
        public static class MagicWrapper {
            private String value;
    
            /* need to proxy */
            public MagicWrapper() {   
            }
    
            public MagicWrapper(String value) {
                this.value = value;
            }
    
            public String get() {
                return this.value;
            }
        }
    
        public static class MagicWrapperFactory implements Factory<MagicWrapper> {
            @Context
            private HttpServletRequest request;
    
            @Override
            public MagicWrapper provide() {
                return new MagicWrapper(request.getParameter("value"));
            }
    
            @Override
            public void dispose(MagicWrapper magic) {}
        }
    
        public static class MagicValueFactoryProvider implements ValueFactoryProvider {
    
            @Inject
            private ServiceLocator locator;
    
            @Override
            public Factory<?> getValueFactory(Parameter parameter) {
                if (parameter.isAnnotationPresent((MagicAnnotation.class))) {
                    final MagicWrapperFactory factory = new MagicWrapperFactory();
                    locator.inject(factory);
                    return factory;
                }
                return null;
            }
    
            @Override
            public PriorityType getPriority() {
                return Priority.NORMAL;
            }
        }
    
        public static class MagicInjectionResolver implements InjectionResolver<MagicAnnotation> {
    
            @Inject @Named(InjectionResolver.SYSTEM_RESOLVER_NAME)
            private InjectionResolver<Inject> systemResolver;
    
            @Override
            public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
                if (injectee.getRequiredType() == MagicWrapper.class) {
                    return systemResolver.resolve(injectee, handle);
                }
                return null;
            }
    
            @Override
            public boolean isConstructorParameterIndicator() { return false; }
    
            @Override
            public boolean isMethodParameterIndicator() { return true; }
        }
    
        @Test
        public void testInjectionsOk() {
            final Response response = target("test").queryParam("value", "HelloWorld")
                    .request().get();
            assertEquals("HelloWorld", response.readEntity(String.class));
            assertEquals("HelloWorld", response.getHeaderString("X-Magic-Header"));
        }
    }
    

    See Also: