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
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"));
}
}