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?
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();
}
}
}
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 MagicWrapper
s. 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:
InjectionResolver
see here and here