Java/Jersey - 使用 ParamInjectionResolver 创建自己的注入解析器 - 奇怪的行为

2022-01-21 00:00:00 dependency-injection spring java jersey hk2

我正在尝试创建一个注入解析器.我有一个数据类:

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

public class MyData {
    ...
}

我有以下注释:

@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 ...
        }
    }
}

我的绑定如下:

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

为了简洁起见,我将实际工厂的实现放在一边.一切正常,但我注意到一些我无法解释的行为.我正在使用以下 JAX-RS 资源进行测试:

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();
    }
}

  • 我注意到的第一件事是 MyDataValueFactoryProvider.createValueFactory 在启动期间被调用了两次.这是为什么?这闻起来像一些错误.好处是工厂只在客户端发出请求时访问一次.
  • 另一个观察结果是,如果我删除资源中的 @MyDataInject 注释,如下所示 (*),MyDataValueFactoryProvider.createValueFactory 仍然会被调用.这是为什么?这很奇怪,因为它应该只绑定到 @MyDataInject?(update) 当参数不属于 MyData 类时,它甚至会被调用,参见下面的第二个变体.
    • 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.
    • (*) 没有 @MyDataInject 注释的资源:

      (*) 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();
          }
      }
      

      另一种变体:

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

      推荐答案

      在启动时,Jersey 会构建所有资源的内部模型.Jersey 使用此模型来处理请求.该模型的一部分由所有资源方法及其所有参数组成.更进一步,Jersey 还将验证模型以确保它是有效的模型.模型中的某些无效内容可能会导致 Jersey 在运行时无法处理该模型.所以这个验证是为了保护我们.

      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.

      话虽如此,验证过程的一部分是验证方法参数.有一些规则可以控制我们可以拥有的参数.例如,@QueryParam 参数必须满足 javadoc:

      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. 成为原始类型
      2. 有一个接受单个字符串参数的构造函数
      3. 有一个名为 valueOffromString 的静态方法,它接受单个 String 参数(例如,参见 Integer.valueOf(String))
      4. 拥有一个已注册的 ParamConverterProvider JAX-RS 扩展 SPI 实现,它返回一个能够对类型进行来自字符串"转换的 ParamConverter 实例.
      5. ListSetSortedSet,其中T满足2、3 或 4 以上.生成的集合是只读的.
      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.

      这里有一些你可以尝试的东西.使用以下任意类添加 @QueryParam

      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) {}
      

      请注意,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 ...
      

      这意味着模型验证失败,因为 Jersey 不知道如何从查询参数创建 Dummy 实例,因为它不遵循允许的规则.

      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.

      好的.那么这一切与您的问题有什么关系?好吧,所有参数注入都需要 ValueFactoryProvider 才能为其提供值.如果没有,则无法在运行时创建参数.因此 Jersey 通过检查是否存在返回 FactoryValueFactoryProvider 来验证参数.Jersey 在运行时调用来获取 Factory 的方法,就是你提到的那个:createValueFactory.

      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.

      现在请记住,当我们实现 createValueFactory 时,我们可以返回 Factory 也可以返回 null.我们应该如何实现它,是检查 Parameter 参数以查看我们是否可以处理该参数.比如

      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;
      }
      

      所以我们在这里告诉 Jersey 这个 ValueFactoryProvider 可以处理什么.在这种情况下,我们可以处理 Dummy 类型的参数,并且如果参数使用 @MyAnnotation 进行注释.

      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.

      那么在启动验证期间会发生什么,对于每个参数,Jersey 会遍历每个注册的 ValueFactoryProvider 以查看是否有一个可以处理该参数.它可以知道的唯一方法是它是否调用了 createValueFactory 方法.如果有一个返回 Factory,那么它是成功的.如果遍历了所有的ValueFactoryProvider都返回null,那么模型是无效的,我们会得到模型验证异常.需要注意的是,有一堆内部的 ValueFactoryProviders 用于使用 @QueryParam@PathParam 等注释进行注释的参数.

      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.

相关文章