11

I am in the process of migrating from DropWizard 0.7.1 to 0.8.1. This includes migrating from Jersey 1.x to 2.x. In my implementation that uses Jersey 1.18.1, I had a MyProvider (changed all class names for simplicity's sake) that implements InjectableProvider. This class would create MyInjectable objects, containing the custom injection annotation, MyToken. MyToken contains various attributes that are passed on and read by MyInjectable. Lastly, in the Application class I register a new instance of MyProvider, as seen below.

I've done some research and can't seem to wrap my head around on how I'd recreate (or replace, I suppose) such a secenario in Jersey 2.x.

Here is the current, 1.18.1 implementation:

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

        // Custom annotation containing various attributes
        boolean someAttribute() default true;
        // ...
    }

    public class MyProvider implements InjectableProvider<MyToken, Parameter> {

        // io.dropwizard.auth.Authenticator
        private final Authenticator<String, MyObject> authenticator;

        public MyProvider(Authenticator<String, MyObject> authenticator) {
            this.authenticator = authenticator;
        }

        @Override
        public ComponentScope getScope() {
            return ComponentScope.PerRequest;
        }

        @Override
        public Injectable<?> getInjectable(ComponentContext ic, MyToken t, Parameter p) {
            return new MyInjectable(authenticator, t.someAttribute());      
        }
    }

    class MyInjectable extends AbstractHttpContextInjectable<MyObject> {
        private final Authenticator<String, Session> authenticator;
        private final boolean someAttribute;

        public MyInjectable(Authenticator<String, MyObject> authenticator, boolean someAttribute) {
            this.authenticator = authenticator;
            this.someAttribute = someAttribute;
            // ... Removed a few paramters for simplicity's sake
        }

        @Override
        public MyObject getValue(HttpContext c) {
            final HttpRequestContext request = c.getRequest();
            // ... Removed code not pertaining to the question
            return myObject;
        }
    }

// Lastly, the register call in the io.dropwizard.Application class
environment.jersey().register(new MyProvider(new MyProviderValidator(someValidator)));
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
Eric Bernier
  • 459
  • 1
  • 7
  • 17

1 Answers1

28

Yeah Jersey made the creation of custom injections a bit more complicated in 2.x. There are a few main components to custom injection you need to know about with Jersey 2.x

You can read more about custom injection in Custom Injection and Lifecycle Management. One shortcoming of the documentation is the lack of explanation of how to inject parameter values. You could get away with simply implementing the InjectResolver, and you would be able to inject into fields with your custom annotation, but in order to inject into method parameters, we need to ValueFactoryProvider.

Luckily there are some abstract classes we can extend (which the documentation also fails to mention) that will make life a little easier. I has to scour the source code of the org.glassfish.jersey.server.internal.inject package for a bit to try and figure it all out.

Here's a full example to help get you started.

Token (injectable object)

public class Token {
    private final String token;
    public Token(String token) { this.token = token; }
    public String getToken() { return token; }
}

@TokenParam (our injection annotation)

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.FIELD})
public @interface TokenParam {
    boolean someAttribute() default true;
}

TokenFactory (implements Factory per the first bullet point, but we just extend the AbstractContainerRequestValueFactory. There we'll have access to the ContainerRequestContext. Note, that all these HK2 components, we can inject other dependencies into them, for example the TokenAuthenticator, which we will bind to HK2 later.

import javax.inject.Inject;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory;

public class TokenFactory extends AbstractContainerRequestValueFactory<Token> {
    
    private final TokenAuthenticator tokenAuthenticator;
    
    @Inject
    public TokenFactory(TokenAuthenticator tokenAuthenticator) {
        this.tokenAuthenticator = tokenAuthenticator;
    }
    
    @Override
    public Token provide() {
        String auth = getContainerRequest().getHeaderString(HttpHeaders.AUTHORIZATION);
        try {
            if (tokenAuthenticator.authenticate(auth).get() == null) {
                throw new WebApplicationException(Response.Status.FORBIDDEN);
            }
        } catch (AuthenticationException ex) {
            Logger.getLogger(TokenFactory.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        return new Token("New Token");
    }  
}

TokenParamInjectionResolver (implements the InjectResolver per bullet point two. I simply extend ParamInjectionResolver. If your interested in what's going on under the hood, you can find the class in the source code I linked to)

import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver;

public class TokenParamInjectionResolver extends ParamInjectionResolver {
    public TokenParamInjectionResolver() {
        super(TokenFactoryProvider.class);
    }
}

TokenFactoryProvider (implements the ValueFactoryProvider per the third bullet point. I simply extend AbstractValueFactoryProvider. Again, you can look at the source for the under the hood details)

import javax.inject.Inject;
import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider;
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
import org.glassfish.jersey.server.model.Parameter;

public class TokenFactoryProvider extends AbstractValueFactoryProvider {
    
    private final TokenFactory tokenFactory;
    
    @Inject
    public TokenFactoryProvider(
            final MultivaluedParameterExtractorProvider extractorProvider,
            ServiceLocator locator,
            TokenFactory tokenFactory) {
        
        super(extractorProvider, locator, Parameter.Source.UNKNOWN);
        this.tokenFactory = tokenFactory;
    }

    @Override
    protected Factory<?> createValueFactory(Parameter parameter) {
         Class<?> paramType = parameter.getRawType();
         TokenParam annotation = parameter.getAnnotation(TokenParam.class);
         if (annotation != null && paramType.isAssignableFrom(Token.class)) {
             return tokenFactory;
         }
         return null;
    }
}

TokenFeature (Here we bind all the components seen above, even the TokenAuthentictor, which I have left out, but if your usual Dropwizard Authenticator. I also made use of a Feature. I tend to do this to wrap components of a custom feature. This is also where you can decide all the scoping. Just note some components are required to be in Singleton scope)

import javax.inject.Singleton;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import org.glassfish.hk2.api.InjectionResolver;
import org.glassfish.hk2.api.TypeLiteral;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider;

public class TokenFeature implements Feature {

    @Override
    public boolean configure(FeatureContext context) {
        context.register(new AbstractBinder(){
            @Override
            public void configure() {
                bind(TokenAuthenticator.class)
                        .to(TokenAuthenticator.class)
                        .in(Singleton.class);
                bind(TokenFactory.class).to(TokenFactory.class)
                        .in(Singleton.class);
                bind(TokenFactoryProvider.class)
                        .to(ValueFactoryProvider.class)
                        .in(Singleton.class);
                bind(TokenParamInjectionResolver.class)
                        .to(new TypeLiteral<InjectionResolver<TokenParam>>(){})
                        .in(Singleton.class);
            }
        });
        return true;
    } 
}

And finally simply register the feature

register(TokenFeature.class);

Now you should be able to inject the Token with @TokenParam, as well as your usual entity bodies (which would not be possible if we didn't implement the ValueFactoryProvider

@POST
@Consumes(MediaType.APPLICATION_JSON)
public String postToken(@TokenParam Token token, User user) {
    
}

UPDATE

It's kind of a half-@$$ example for your particular use case. A better approach would probably have a clone method in your Factory class and create a new TokenFactory with some parameters (maybe that you get from your annotation. For example, in the TokenFactory` you can have something like

public class TokenFactory extends AbstractContainerRequestValueFactory<Token> {

    public TokenFactory clone(boolean someAttribute) {
        return new TokenFactory(authenticator, someAttribute);
    }

In the TokenFactoryProvider ine createValueFactory method, you then call the clone method

TokenParam annotation = parameter.getAnnotation(TokenParam.class);

if (annotation != null && paramType.isAssignableFrom(Token.class)) {
    return tokenFactory.clone(annotation.someAttribute());
}

Or you could actually create the factory inside the method. you have options.

UPDATE 2

See Also

UPDATE 3

Starting Jersey 2.26, the dependency injection has changed. You will want to look at this post for example of how the code has changed in implementing this same injection.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • You deleted your comments on my other question, so I'll ask here. Since I'm trying to inject a field onto an `@BeanParam`, is that why I need the `ValueFactoryProvider`? Because from the documentation around the `InjectionResolver`, I got the strong implication that it would be sufficient to inject a field onto a BeanParam, since it is a standalone object. Regardless, thanks for the help and I'm going to try the Value Factory widget. – Patrick M Jun 16 '15 at 13:51
  • @PatrickM I deleted because I was unsure (I didn't do any testing and have never tried to inject in to a @BeanParam bean). But for method parameters it appears we need to use `ValueFactoryProvider`. It seems the `InjectionResolver` is not enough. Since it seems to apply to method params, I though it might apply to your bean fields also. I may be wrong, but doesn't hurt to give it a shot. – Paul Samsotha Jun 16 '15 at 13:57
  • I found this answer to be quite useful but also a bit confusing. I tried getting it to work, but was always greated with "No injection source found for a parameter" errors. I Followed the jersey source example of https://github.com/jersey/jersey/blob/927b4d0605aeb119d15e9623dab65c8aec2c8e20/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/HeaderParamValueFactoryProvider.java I also made an example project: https://github.com/evadnoob/jersey2-method-param-injection – David Oct 05 '16 at 18:20
  • Is it possible to inject with @Context annotation? I cannot get it working. I do exact stuff you've described except for Resolver, since ContextInjectionResolver is already configured by jersey framework – user1745356 Jan 12 '17 at 20:53