So when you instantiate the resource to make it a singleton, Jersey tries to do all the injection on startup. This means that attempt to access any object that is inherently request scoped, will fail... UNLESS... the object is proxiable.
Some objects are made proxiable by Jersey, and this is by design, and by specification. For example HttpHeaders
, UriInfo
, SecurityContext
, and a few others listed here. Though HttpServletRequest
is not listed, it is also one of the objects that are proxiable.
What it means to be proxiable is that instead of injecting the actual object (which doesn't exist until there is request), a proxy is injected. When calls are made on the proxy, they get forwarded to the actual object that is available in the current request. You can try to print/log the class of the HttpServletRequest
and you will see that the class is actually com.sun.proxy.ProxyX
instead of HttpServletRequestSomeImpl
. This is the magic of Java's dynamic proxies at work.
The current problem you are facing is the injection of Datastore
. It is inherently request scoped because it's creation in dependent on request context information, i.e. the headers. So during injection, it fails on this call to obtain the ContainerRequest
inside your factory
ContainerRequest request = getContainerRequest();
The error message being "Not inside a request scope", which makes perfect sense as there is no request when we try to obtain it.
So how can we fix this? Well we need to make the Datastore
proxiable.
Generally, the way you can do this is by configuring it during binding declaration, for example
bindFactory(...).proxy(true).proxyForSameScope(false).to(...);
The proxy(true)
method makes it proxiable, and the proxyForSameScope(false)
says that if we are trying to inject into the same scope, it should not be a proxy, but the actual instance.
One problem with your current configuration is that you are binding the factory to the factory
bind(TenantDatastoreFactory.class)
.to(TenantDatastoreFactory.class)
.in(Singleton.class);
This makes sense for your current implementation, as you are trying to inject the factory into the TenantDatastoreFactoryProvider
. But what we actually need to make the proxying work, is for the factory to be binded to the actual Datastore
:
bindFactory(TenantDatastoreFactory.class)
.proxy(true)
.proxyForSameScope(false)
.to(Datastore.class)
.in(RequestScoped.class);
So now we have taken out the binding of the factory, we can't inject it. So we just have to problem of returning a Factory
from the createValueFactory
method. We don't want to just return TenantDatastoreFactory
instance because we'll still face the same problem where the provide
method is called to get the Datastore
. To get around this, we can do the following
@Override
protected Factory<?> createValueFactory(Parameter parameter) {
Class<?> paramType = parameter.getRawType();
TenantDatastore annotation = parameter.getAnnotation(TenantDatastore.class);
if (annotation != null && paramType.isAssignableFrom(Datastore.class)) {
return getFactory();
}
return null;
}
private Factory<Object> getFactory() {
return new Factory<Object>() {
@Context
Datastore datastore;
@Override
public Object provide() {
return datastore;
}
@Override
public void dispose(Object t) {}
};
}
So we are creating a Factory
dynamically, where we inject the proxied Datastore
. Now when Jersey tries to inject the resource class, it will inject the proxy, and the provide
method is never called on start up. It is only called when we try o actually use the Datastore
during the request.
It may seem redundant, that we have both the TenantDatastoreFactory
and the anonymous Factory
created as runtime. But this is necessary to make the Datastore
proxiable and make sure the provide()
method is never called on startup.
Another note, is that if you don't require parameter injection, you could've simplified the implementation by taking out the TenantDatastoreFactoryProvider
. This is only required for parameter injection. All we need is the InjectionResolver
to handle the custom annotation, and the factory to create the Datastore
. The InjectionResolver
implementation would need to be changed as follows
public class TenantDatastoreInjectionResolver
implements InjectionResolver<TenantDatastore> {
@Inject
@Named(InjectionResolver.SYSTEM_RESOLVER_NAME)
InjectionResolver<Inject> systemInjectionResolver;
@Override
public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
if (Datastore.class == injectee.getRequiredType()) {
return systemInjectionResolver.resolve(injectee, handle);
}
return null;
}
@Override
public boolean isConstructorParameterIndicator() { return false; }
@Override
public boolean isMethodParameterIndicator() { return false; }
}
Then in the binder, just take out the TenantDatastoreFactoryProvider
@Override
public void configure() {
bindFactory(TenantDatastoreFactory.class)
.proxy(true)
.proxyForSameScope(false)
.to(Datastore.class)
.in(RequestScoped.class);
bind(TenantDatastoreInjectionResolver.class)
.to(new TypeLiteral<InjectionResolver<TenantDatastore>>() {
})
.in(Singleton.class);
}
Again this is only if you don't require parameter injection.
See Also