Search code examples
javajerseyjersey-2.0interceptorhk2

@Inject not working in MethodInterceptor managed by HK2


I am currently refactoring a Jersey Web App and want to bundle some cross cutting concerns in their own classes and use annotations to intercept methods. For example there are a lot of methods where I need to check if the user is the owner of the entity he wants to alter (in my case this is a project). Therefor within the interceptor I need to make database calls and though having the appropriate DAOs injected would be the best way to go.

Currently my interceptor looks like this:

 public class ProjectOwnerCheckInterceptor implements MethodInterceptor {

        @Inject
        private EntityManager em;

        @Inject
        private UserProvider userProvider;

        @Inject
        private RMUserDAO rmUserDAO;

        @Inject
        private ProjectDAO projectDAO;

        public ProjectOwnerCheckInterceptor() {
        // TODO Auto-generated constructor stub
        }

        @Override
        public Object invoke(MethodInvocation arg0) throws Throwable {

            // First of all let's get the annotation
            ProjectOwnerCheck check = arg0.getMethod().getAnnotation(ProjectOwnerCheck.class);

            // if there is no check, then just proceed!
            if (check == null)
                arg0.proceed();

            long projectId = (long) arg0.getArguments()         [check.projectIdIndex()];

            // Handling ownership!!
            Project project = getProjectOrThrow(projectId);

            return arg0.proceed();

        }
    }

The custom annotation is straight forward. I need to add some little information which argument position the entityId in the method has to check is as number of parameters and types vary:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface ProjectOwnerCheck {

    int projectIdIndex() default -1;

}

For Jersey/HK2 to know what to do with the interceptors I have created a filter implementing InterceptionService:

public class HK2InterceptorFilter implements InterceptionService {

    private final static MethodInterceptor PROJECT_CHECK_METHOD_INTERCEPTOR = new ProjectOwnerCheckInterceptor();
    private final static List<MethodInterceptor> PROJECT_CHECK_METHOD_LIST = Collections
            .singletonList(PROJECT_CHECK_METHOD_INTERCEPTOR);

    public HK2InterceptorFilter() {
        // TODO Auto-generated constructor stub
    }

    @Override
    public Filter getDescriptorFilter() {

        return BuilderHelper.allFilter();

    }

    @Override
    public List<MethodInterceptor> getMethodInterceptors(Method method) {

        if (method.isAnnotationPresent(ProjectOwnerCheck.class))
            return PROJECT_CHECK_METHOD_LIST;

        return null;

    }

    @Override
    public List<ConstructorInterceptor> getConstructorInterceptors(Constructor<?> constructor) {
        // TODO Auto-generated method stub
        return null;
    }

}

I am binding that Filter in my JerseyApplication class:

register(new AbstractBinder() {
            @Override
            protected void configure() {
                try {

                    bind(HK2InterceptorFilter.class).to(InterceptionService.class).in(Singleton.class);
                    bind(getPasswordStorage()).to(PasswordStorage.class);
                    bind(getDocumentService()).to(DocumentService.class);
                    bind(UserManagementAccessor.getUserProvider()).to(UserProvider.class);
                } catch (Exception e) {
                    throw new InternalServerErrorException(e);
                }
            }
        });

When set a breakpoint in my interceptor I can see that it is correctly instantiated and the method gots called. But what I totally miss are all those @Inject fields I need to have to make my check. Am I missing something or is this in HK2 not possible. I used to work with Guice and there it is working (I am - due to the fact the app's code base is quite large, but time limited - bound to HK2:) ).

Thanks for all your help in advance!

PS:

I am using Jersey 2.17


Solution

  • The problem is the interceptor never goes through the DI lifecycle because if is not a service in the container. You are instantiating it yourself. When you do this in a DI framework, the majority of the time you can expect this result.

    What you can do though is explicitly inject it yourself using HK2 container, the ServiceLocator. You inject the locator into the InterceptionService, and then call locator.inject(interceptor). This method is a general purpose method to explicitly inject any arbitrary object. So you might change it something like

    private final List<MethodInterceptor> PROJECT_CHECK_METHOD_LIST;
    
    @Inject
    public HK2InterceptorFilter(ServiceLocator locator) {
        final MethodIntercator i = new ProjectOwnerCheckInterceptor();
        locator.inject(i)
        PROJECT_CHECK_METHOD_LIST = Collections.singletonList(i);
    }
    

    The other problem you are going to face is that the interceptor is a singleton, but all your service you are trying to inject into look like they are request scoped. This is a problem, as they need to change on a per request basis. For this what we can do is make them proxies. We can do this simply by chaining a couple method in the binding

    bind(getPasswordStorage())
        .to(PasswordStorage.class)
        .proxy(true)
        .proxyForSameScope(false)
        .in(RequestScoped.class);
    

    See Also: Injecting Request Scoped Objects into Singleton Scoped Object with HK2 and Jersey

    Below is a complete example using Jersey Test Framework.

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Method;
    import java.util.Collections;
    import java.util.List;
    import java.util.logging.Logger;
    
    import javax.inject.Inject;
    import javax.inject.Singleton;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.core.Context;
    import javax.ws.rs.core.HttpHeaders;
    import javax.ws.rs.core.Response;
    
    import org.aopalliance.intercept.ConstructorInterceptor;
    import org.aopalliance.intercept.MethodInterceptor;
    import org.aopalliance.intercept.MethodInvocation;
    import org.glassfish.hk2.api.Filter;
    import org.glassfish.hk2.api.InterceptionService;
    import org.glassfish.hk2.api.ServiceLocator;
    import org.glassfish.hk2.utilities.BuilderHelper;
    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.test.JerseyTest;
    import org.junit.Test;
    
    import static org.hamcrest.CoreMatchers.containsString;
    import static org.hamcrest.CoreMatchers.is;
    import static org.junit.Assert.assertThat;
    
    /**
     * Stack Overflow question http://stackoverflow.com/q/36859669/2587435
     * 
     * Run this like any other JUnit test. One one required test dependency:
     * 
     * <dependency>
     *     <groupId>org.glassfish.jersey.test-framework.providers</groupId>
     *     <artifactId>jersey-test-framework-provider-inmemory</artifactId>
     *     <version>${jersey2.version}</version>
     * </dependency>
     *
     * @author Paul Samsotha
     */
    public class InterceptionTest extends JerseyTest {
    
        public static interface HeaderProvider {
            String getXCustomHeader();
        }
    
        public static class HeaderProviderImpl implements HeaderProvider {
            @Context
            private HttpHeaders headers;
    
            @Override
            public String getXCustomHeader() {
                return headers.getHeaderString("X-Custom-Header");
            }
        }
    
        @Target(ElementType.METHOD)
        @Retention(RetentionPolicy.RUNTIME)
        public static @interface Intercept {
        }
    
        public static class MyMethodInterceptor implements MethodInterceptor {
    
            @Inject
            private HeaderProvider provider;
    
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                return provider.getClass().getName() + ":" + provider.getXCustomHeader();
            }
    
        }
    
        public static class InterceptionHandler implements InterceptionService {
    
            private final List<MethodInterceptor> interceptors;
    
            @Inject
            public InterceptionHandler(ServiceLocator locator) {
                final MethodInterceptor interceptor = new MyMethodInterceptor();
                locator.inject(interceptor);
                interceptors = Collections.singletonList(interceptor);
            }
    
            @Override
            public Filter getDescriptorFilter() {
                return BuilderHelper.allFilter();
            }
    
            @Override
            public List<MethodInterceptor> getMethodInterceptors(Method method) {
                if (method.isAnnotationPresent(Intercept.class)) {
                    return interceptors;
                }
                return null;
            }
    
            @Override
            public List<ConstructorInterceptor> getConstructorInterceptors(Constructor<?> c) {
                return null;
            }
        }
    
        public static class Binder extends AbstractBinder {
            @Override
            protected void configure() {
                bind(InterceptionHandler.class)
                        .to(InterceptionService.class)
                        .in(Singleton.class);
                bind(HeaderProviderImpl.class)
                        .to(HeaderProvider.class)
                        .proxy(true)
                        .proxyForSameScope(false)
                        .in(RequestScoped.class);
            }
        }
    
        @Path("intercept")
        public static class TestResource {
    
            @GET
            @Intercept
            public String get() {
                return null;
            }
        }
    
        @Override
        public ResourceConfig configure() {
            return new ResourceConfig(TestResource.class)
                    .register(new Binder())
                    .register(new LoggingFilter(Logger.getAnonymousLogger(), true));          
        }
    
        @Test
        public void shouldReturnHeaderAndProxyClass() {
            Response response = target("intercept").request()
                    .header("X-Custom-Header", "Value1")
                    .get();
            assertThat(response.getStatus(), is(200));
            String entity = response.readEntity(String.class);
            response.close();
            assertThat(entity, containsString("Value1"));
            assertThat(entity, containsString("Proxy"));
    
            // Change header to make sure we aren't getting the same HttpHeaders instance
            response = target("intercept").request()
                    .header("X-Custom-Header", "Value2")
                    .get();
            assertThat(response.getStatus(), is(200));
            entity = response.readEntity(String.class);
            response.close();
            assertThat(entity, containsString("Value2"));
            assertThat(entity, containsString("Proxy"));
        }
    }