问题
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
ValueFactoryProvider
directly, we extendAbstractValueFactoryProvider
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
Factory
that provides the method parameter value.The
InjectionResolver
is what is used to handle the custom annotation. With this pattern, instead of directly implementing it, as the OP has, we just extendParamInjectionResolver
passing in ourAbstractValueFactoryProvider
implementation class to super constructorpublic 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