问题
Hello I am building an application using dropwizard, that is using jersey 2.16 internally as REST API framework.
For the whole application on all resource methods I need some information so to parse that information I defined a custom filter like below
@java.lang.annotation.Target(ElementType.PARAMETER)
@java.lang.annotation.Retention(RetentionPolicy.RUNTIME)
public @interface TenantParam {
}
The tenant factory is defined below
public class TenantFactory implements Factory<Tenant> {
    private final HttpServletRequest request;
    private final ApiConfiguration apiConfiguration;
    @Inject
    public TenantFactory(HttpServletRequest request, @Named(ApiConfiguration.NAMED_BINDING) ApiConfiguration apiConfiguration) {
        this.request = request;
        this.apiConfiguration = apiConfiguration;
    }
    @Override
    public Tenant provide() {
        return null;
    }
    @Override
    public void dispose(Tenant tenant) {
    }
}
I haven't actually implemented the method but structure is above. There is also a TenantparamResolver
public class TenantParamResolver implements InjectionResolver<TenantParam> {
    @Inject
    @Named(InjectionResolver.SYSTEM_RESOLVER_NAME)
    private InjectionResolver<Inject> systemInjectionResolver;
    @Override
    public Object resolve(Injectee injectee, ServiceHandle<?> serviceHandle) {
        if(Tenant.class == injectee.getRequiredType()) {
            return systemInjectionResolver.resolve(injectee, serviceHandle);
        }
        return null;
    }
    @Override
    public boolean isConstructorParameterIndicator() {
        return false;
    }
    @Override
    public boolean isMethodParameterIndicator() {
        return true;
    }
}
Now in my resource method I am doing like below
@POST
@Timed
public ApiResponse create(User user, @TenantParam Tenant tenant) {
    System.out.println("resource method invoked. calling service method");
    System.out.println("service class" + this.service.getClass().toString());
    //DatabaseResult<User> result = this.service.insert(user, tenant);
    //return ApiResponse.buildWithPayload(new Payload<User>().addObjects(result.getResults()));
    return null;
}
Here is how I am configuring the application
@Override
public void run(Configuration configuration, Environment environment) throws Exception {
    // bind auth and token param annotations
    environment.jersey().register(new AbstractBinder() {
        @Override
        protected void configure() {
            bindFactory(TenantFactory.class).to(Tenant.class);
            bind(TenantParamResolver.class)
                .to(new TypeLiteral<InjectionResolver<TenantParam>>() {})
                .in(Singleton.class);
        }
    });
}
The problem is during application start I am getting below error
WARNING: No injection source found for a parameter of type public void com.proretention.commons.auth.resources.Users.create(com.proretention.commons.api.core.Tenant,com.proretention.commons.auth.model.User) at index 0.
and there is very long stack error stack and description
Below is the declaration signature of user pojo
public class User extends com.company.models.Model {
No annotations on User class. Model is a class that defines only single property id of type long and also no annotations on model class
When I remove the User parameter from above create resource method it works fine and when I removed TenantParam it also works fine. The problem only occurs when I use both User and TenantParam
- What I am missing here ? how to resolve this error ?
EDITED
I just tried with two custom method param injection, that is also not working
@POST
@Path("/login")
@Timed
public void validateUser(@AuthParam AuthToken token, @TenantParam Tenant tenant) {
}
- What I am missing here ? Is this a restriction in jersey ?
回答1:
Method parameters are handled a little differently for injection. The component we need to implement for this, is the ValueFactoryProvider. Once you implement that, you also need to bind it in your AbstractBinder.
Jersey has a pattern that it follows for implementing the ValueFactoryProvider. This is the pattern used to handle parameters like @PathParam and @QueryParam. Jersey has a ValueFactoryProvider for each one of those, as well as others.
The pattern is as follows:
- Instead of implementing the - ValueFactoryProviderdirectly, we extend- AbstractValueFactoryProvider- public static class TenantValueProvider extends AbstractValueFactoryProvider { @Inject public TenantValueProvider(MultivaluedParameterExtractorProvider mpep, ServiceLocator locator) { super(mpep, locator, Parameter.Source.UNKNOWN); } @Override protected Factory<?> createValueFactory(Parameter parameter) { if (!parameter.isAnnotationPresent(TenantParam.class) || !Tenant.class.equals(parameter.getRawType())) { return null; } return new Factory<Tenant>() { @Override public Tenant provide() { ... } }; }- In this component, it has a method we need to implement that returns the - Factorythat provides the method parameter value.
- The - InjectionResolveris what is used to handle the custom annotation. With this pattern, instead of directly implementing it, as the OP has, we just extend- ParamInjectionResolverpassing in our- AbstractValueFactoryProviderimplementation class to super constructor- public static class TenantParamInjectionResolver extends ParamInjectionResolver<TenantParam> { public TenantParamInjectionResolver() { super(TenantValueProvider.class); } }
And that's really it. Then just bind the two components
public static class Binder extends AbstractBinder {
    @Override
    public void configure() {
        bind(TenantParamInjectionResolver.class)
                .to(new TypeLiteral<InjectionResolver<TenantParam>>(){})
                .in(Singleton.class);
        bind(TenantValueProvider.class)
                .to(ValueFactoryProvider.class)
                .in(Singleton.class);
    }
}
Below is a complete test using Jersey Test Framework. The required dependencies are listed in the javadoc comments. You can run the test 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.Singleton;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Response;
import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.api.InjectionResolver;
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.server.ContainerRequest;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory;
import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider;
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver;
import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
 * Stack Overflow https://stackoverflow.com/q/29145807/2587435
 * 
 * Run this like any other JUnit test. Dependencies required are as the following
 * 
 *  <dependency>
 *      <groupId>org.glassfish.jersey.test-framework.providers</groupId>
 *      <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
 *      <version>2.22</version>
 *      <scope>test</scope>
 *  </dependency>
 *  <dependency>
 *      <groupId>org.glassfish.jersey.media</groupId>
 *      <artifactId>jersey-media-json-jackson</artifactId>
 *      <version>2.22</version>
 *      <scope>test</scope>
 *  </dependency>
 * 
 * @author Paul Samsotha
 */
public class TenantInjectTest extends JerseyTest {
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface TenantParam {
    }
    public static class User {
        public String name;
    }
    public static class Tenant {
        public String name;
        public Tenant(String name) {
            this.name = name;
        }
    }
    public static class TenantValueProvider extends AbstractValueFactoryProvider {
        @Inject
        public TenantValueProvider(MultivaluedParameterExtractorProvider mpep,
                                   ServiceLocator locator) {
            super(mpep, locator, Parameter.Source.UNKNOWN);
        }
        @Override
        protected Factory<?> createValueFactory(Parameter parameter) {
            if (!parameter.isAnnotationPresent(TenantParam.class) 
                    || !Tenant.class.equals(parameter.getRawType())) {
                return null;
            }
            return new AbstractContainerRequestValueFactory<Tenant>() {
                // You can @Inject things here if needed. Jersey will inject it.
                // for example @Context HttpServletRequest
                @Override
                public Tenant provide() {
                    final ContainerRequest request = getContainerRequest();
                    final String name 
                            = request.getUriInfo().getQueryParameters().getFirst("tenent");
                    return new Tenant(name);
                }
            };
        }
        public static class TenantParamInjectionResolver 
                extends ParamInjectionResolver<TenantParam> {
            public TenantParamInjectionResolver() {
                super(TenantValueProvider.class);
            }
        } 
        public static class Binder extends AbstractBinder {
            @Override
            public void configure() {
                bind(TenantParamInjectionResolver.class)
                        .to(new TypeLiteral<InjectionResolver<TenantParam>>(){})
                        .in(Singleton.class);
                bind(TenantValueProvider.class)
                        .to(ValueFactoryProvider.class)
                        .in(Singleton.class);
            }
        }
    }
    @Path("test")
    @Produces("text/plain")
    @Consumes("application/json")
    public static class TestResource {
        @POST
        public String post(User user, @TenantParam Tenant tenent) {
            return user.name + ":" + tenent.name;
        }
    }
    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(TestResource.class)
                .register(new TenantValueProvider.Binder())
                .register(new LoggingFilter(Logger.getAnonymousLogger(), true));
    }
    @Test
    public void shouldReturnTenantAndUserName() {
        final User user = new User();
        user.name = "peeskillet";
        final Response response = target("test")
                .queryParam("tenent", "testing")
                .request()
                .post(Entity.json(user));
        assertEquals(200, response.getStatus());
        assertEquals("peeskillet:testing", response.readEntity(String.class));
    }
}
See Also:
- Jersey 2.x Custom Injection Annotation With Attributes
- My Comment in the Dropwizard issue: "No injection source found for a parameter"
来源:https://stackoverflow.com/questions/29145807/jersey-custom-method-parameter-injection-with-inbuild-injection