Jaap Dekkers Jaap Dekkers - 2 months ago 22
Java Question

Java/Jersey - creating own injection resolver with ParamInjectionResolver - strange behavior

I am trying to create an injection resolver. I have a data class:

public class MyData {
...
}


I have the following annotation:

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


My injection resolver looks like this:

public class MyDataInjectionResolver extends ParamInjectionResolver<MyDataInject> {
public MyDataInjectionResolver () {
super(MyDataValueFactoryProvider.class);
}

@Singleton
public static class MyDataValueFactoryProvider extends AbstractValueFactoryProvider {
@Inject
public MyDataValueFactoryProvider(MultivaluedParameterExtractorProvider provider, ServiceLocator locator) {
super(provider, locator, Parameter.Source.UNKNOWN);
}

@Override
protected Factory<?> createValueFactory(Parameter parameter) {
System.out.println(parameter.getRawType());
System.out.println(Arrays.toString(parameter.getAnnotations()));
System.out.println("------------------------------------------------------------------");
System.out.println();

... create factory and return ...
}
}
}


I am binding as following:

bind(MyDataValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class);
bind(MyDataInjectionResolver.class).to(new TypeLiteral<InjectionResolver<MyDataInject>>() {}).in(Singleton.class);


I left the implementation of the actual factory out for brevity. Everything works fine, but I am noticing some behavior which I cannot explain. I am testing with the following JAX-RS resource:

@Path("test")
public class Test {
@GET
public Response test(@MyDataInject @Valid MyData data) {
return Response.status(Response.Status.OK).entity("Hello world!").build();
}
}



  • The first thing I notice is that
    MyDataValueFactoryProvider.createValueFactory
    is called twice during start-up. Why is that? This smells like some error. The good thing is that the factory is only accessed once when a client does a request.

  • Another observation is that if I remove the
    @MyDataInject
    annotation in the resource as below (*),
    MyDataValueFactoryProvider.createValueFactory
    is still getting called. Why is that? This is odd, since it should be bound to only
    @MyDataInject
    ? (update) It is even called when the parameter is not of class
    MyData
    , see second variant below.



(*) Resource without
@MyDataInject
annotation:

@Path("test")
public class Test {
@GET
public Response test(/*@MyDataInject*/ @Valid MyData data) {
return Response.status(Response.Status.OK).entity("Hello world!").build();
}
}


Another variant:

@Path("test")
public class Test {
@GET
public Response test(@Valid SomeOtherClass data) {
return Response.status(Response.Status.OK).entity("Hello world!").build();
}
}

Answer

On startup, Jersey builds an internal model of all the resources. Jersey uses this model to process requests. Part of that model consists of all the resource methods and all of its parameters. To take it even further, Jersey will also validate the model to make sure it is a valid model. Something invalid in the model may cause Jersey not to be able to process that model during runtime. So this validation is there to protect us.

That being said, part of the validation process is to validate the method parameters. There are rules that govern what we can have as parameters. For example, a @QueryParam parameters must meet one of the requirements mentioned here in the javadoc:

  1. Be a primitive type
  2. Have a constructor that accepts a single String argument
  3. Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String))
  4. Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
  5. Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.

Here's something you can try out. Add a @QueryParam using the following arbitrary class

public class Dummy {
  public String value;
}

@GET
public Response get(@QueryParam("dummy") Dummy dummy) {}

Notice that the Dummy class doesn't meet any of the requirements listed above. When you run the application, you should get an exception on startup, causing the application to fail. The exception will be something like

ModelValidationException: No injection source for parameter ...

This means that the validation of the model failed because Jersey has no idea how to create the Dummy instance from the query param, as it doesn't follow the rules of what is allowed.

Ok. so how is this all related to your question? Well, all parameter injection requires a ValueFactoryProvider to be able to provide a value for it. If there isn't one, then the parameter will not be able to be created at runtime. So Jersey validates the parameters by checking for the existence of a ValueFactoryProvider that returns a Factory. The method that Jersey calls to obtain the Factory at runtime, is the one you mentioned: createValueFactory.

Now keep in mind that when we implement the createValueFactory, we can either return a Factory or we can return null. How we should implement it, is the check to Parameter argument to see if we can handle that parameter. For instance

protected Factory<?> createValueFactory(Parameter parameter) {
   if (parameter.getRawType() == Dummy.class
       && parameter.isAnnotationPresent(MyAnnoation.class)) {
     return new MyFactory();
   }
   return null;
}

So here we are telling Jersey what this ValueFactoryProvider can handle. In this case we can handle parameters of type Dummy and if the parameter is annotated with @MyAnnotation.

So what happens during startup validation, for each parameter, Jersey will traverse each ValueFactoryProvider registered to see if there is one that can handle that parameter. The only way it can know is if it calls the createValueFactory method. If there is one that returns a Factory, then it is a success. If all the ValueFactoryProviders are traversed and they all return null, then the model is not valid and we will get the model validation exception. It should be noted that there are a bunch of internal ValueFactoryProviders for parameter annotated with annotations like @QueryParam, @PathParam, etc.

Comments