基于HttpRequest的Jersey 2上下文注入,不使用单例模式

3
我希望能够为单个请求按字段注入Datastore,例如:
@Context
protected HttpServletRequest request;

目前我已经实现了类似于这个方法的方式: 使用属性的Jersey 2.x自定义注入注释 如下所示:

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

public class TenantDatastoreFactory extends AbstractContainerRequestValueFactory<Datastore> {

    public TenantDatastoreFactory() {}

    @Override
    public Datastore provide() {
        ContainerRequest request = getContainerRequest();
        return DatastoreManager.getDs(request.getHeaders().get("Host")));
    }

    @Override
    public void dispose(Datastore d) {}
}

public class TenantDatastoreFactoryProvider extends AbstractValueFactoryProvider {

    private final TenantDatastoreFactory tenantDatastoreFactory;

    @Inject
    public TenantDatastoreFactoryProvider(
            final MultivaluedParameterExtractorProvider extractorProvider,
            ServiceLocator locator,
            TenantDatastoreFactory tenantDatastoreFactory) {

        super(extractorProvider, locator, Parameter.Source.UNKNOWN);
        this.tenantDatastoreFactory = tenantDatastoreFactory;
    }

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

public class TenantDatastoreInjectionResolver extends ParamInjectionResolver {
    public TenantDatastoreInjectionResolver() {
        super(TenantDatastoreFactoryProvider.class);
    }
}

@Path("/users")
public class User {
    @TenantDatastore
    private Datastore    ds;
    private ObjectMapper objectMapper;

    public User(ObjectMapper objectMapper) {
      this.objectMapper = objectMapper;
    }

    @GET
    public Response getUsers(){
      return Response.ok(ds.find(User.class).asList()).build();
    }
}

在 dropwizard 应用程序的 run 方法中:

environment.jersey().register(new UserResource(objectMapper));

environment.jersey().getResourceConfig().register(new AbstractBinder(){
    @Override
    public void configure() {
        bind(TenantDatastoreFactory.class)
          .to(TenantDatastoreFactory.class)
          .in(Singleton.class);
        bind(TenantDatastoreFactoryProvider.class)
          .to(ValueFactoryProvider.class)
          .in(Singleton.class);
        bind(TenantDatastoreInjectionResolver.class)
          .to(new TypeLiteral<InjectionResolver<TenantDatastore>>(){})
          .in(Singleton.class);
    }
});

我看到,你需要将资源注册为单例,就像这样:

我读到,你必须将资源注册为单例,像这样:

environment.jersey().register(UserResource.class);

但是我必须将对象传递给构造函数,而这在单例模式下是不可能的。javax.servlet.http.HttpServletRequestjavax.ws.rs.core.Context与资源实例一起很好地工作,那么我该如何使这种行为对我的用例可行?

1个回答

9
当您实例化资源以使其成为单例时,Jersey 尝试在启动时进行所有注入。这意味着尝试访问任何固有请求范围的对象将会失败... 除非该对象是可代理的。
一些对象由 Jersey 设计和规定为可代理。例如 HttpHeaders、UriInfo、SecurityContext 和其他一些列在 这里 的对象。虽然 HttpServletRequest 没有列出,但它也是可代理的对象之一。
被代理的意思是,不注入实际对象(因为在请求之前实际对象并不存在),而是注入一个代理。当代理上进行调用时,它们会被转发到当前请求中可用的实际对象上。您可以尝试打印/记录HttpServletRequest的类,您会发现该类实际上是com.sun.proxy.ProxyX,而不是HttpServletRequestSomeImpl。这是Java 动态代理 的魔力所在。
您目前面临的问题是注入Datastore。它本质上是请求范围的,因为它的创建取决于请求上下文信息,即标头。因此,在注入过程中,它无法获取工厂内部的ContainerRequest
ContainerRequest request = getContainerRequest();

错误信息是“未在请求范围内”,这很有道理,因为当我们尝试获取它时没有请求。
那么我们该如何解决呢?好吧,我们需要使Datastore可代理。 通常,您可以通过在绑定声明期间进行配置来实现此操作,例如:
bindFactory(...).proxy(true).proxyForSameScope(false).to(...);

proxy(true)方法使其可代理,proxyForSameScope(false)表示如果我们正在尝试注入到相同的作用域中,则不应该是代理,而是实际实例。

您当前配置中的一个问题是将工厂绑定到自身工厂。

bind(TenantDatastoreFactory.class)
  .to(TenantDatastoreFactory.class)
  .in(Singleton.class);

这对于您当前的实现是有意义的,因为您正在尝试将工厂注入到 TenantDatastoreFactoryProvider 中。但是,为了使代理工作,我们实际上需要将工厂绑定到真正的 Datastore
bindFactory(TenantDatastoreFactory.class)
        .proxy(true)
        .proxyForSameScope(false)
        .to(Datastore.class)
        .in(RequestScoped.class);

现在我们已经取消了工厂的绑定,无法注入它。因此,我们只需要解决从createValueFactory方法返回Factory的问题。我们不想仅返回TenantDatastoreFactory实例,因为仍然会面临调用provide方法获取Datastore的同样问题。为了解决这个问题,我们可以采取以下措施。
@Override
protected Factory<?> createValueFactory(Parameter parameter) {
     Class<?> paramType = parameter.getRawType();
     TenantDatastore annotation = parameter.getAnnotation(TenantDatastore.class);
     if (annotation != null && paramType.isAssignableFrom(Datastore.class)) {
         return getFactory();
     }
     return null;
}

private Factory<Object> getFactory() {
    return new Factory<Object>() {

        @Context
        Datastore datastore;

        @Override
        public Object provide() {
            return datastore;
        }

        @Override
        public void dispose(Object t) {}
    };
}

因此,我们正在动态创建一个Factory,其中注入了代理的Datastore。现在当Jersey尝试注入资源类时,它将注入代理,并且在启动时永远不会调用provide方法。只有在我们在请求期间尝试实际使用Datastore时才会调用它。
看起来可能有些冗余,我们既有运行时创建的TenantDatastoreFactory 匿名Factory。但这是为了使Datastore可代理并确保在启动时永远不会调用provide()方法而必要的。
另一个需要注意的是,如果您不需要参数注入,可以通过取出TenantDatastoreFactoryProvider来简化实现。这仅对参数注入是必需的。我们只需要InjectionResolver处理自定义注释,以及工厂创建DatastoreInjectionResolver实现需要更改如下。
public class TenantDatastoreInjectionResolver 
        implements InjectionResolver<TenantDatastore> {

    @Inject
    @Named(InjectionResolver.SYSTEM_RESOLVER_NAME)
    InjectionResolver<Inject> systemInjectionResolver;

    @Override
    public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
        if (Datastore.class == injectee.getRequiredType()) {
            return systemInjectionResolver.resolve(injectee, handle);
        }
        return null;
    }

    @Override
    public boolean isConstructorParameterIndicator() { return false; }
    @Override
    public boolean isMethodParameterIndicator() { return false; }
}

然后在绑定器中,只需取出TenantDatastoreFactoryProvider

@Override
public void configure() {
    bindFactory(TenantDatastoreFactory.class)
            .proxy(true)
            .proxyForSameScope(false)
            .to(Datastore.class)
            .in(RequestScoped.class);
    bind(TenantDatastoreInjectionResolver.class)
            .to(new TypeLiteral<InjectionResolver<TenantDatastore>>() {
            })
            .in(Singleton.class);
}

如果您不需要参数注入,那么这只是一个参考。

另请参阅


非常好,我想我已经明白了应该发生什么。不幸的是,现在提供只被调用一次,而不是在未来的请求中。 :-/ - sclausen
1
尝试将TenantDatastoreFactory绑定到RequestScoped.class中。 - Paul Samsotha
但是RequestScope不是一个注释,bindFactory(TenantDatastoreFactory.class) .proxy(true) .proxyForSameScope(false) .to(Datastore.class) .in(RequestScope.class);将无法工作。 - sclausen
1
如果您将资源注册为类,则在首次请求之前不会实例化。与实例不同,它已经实例化,因此所有的注入都会在启动时发生,而不是像类一样直到实例化(在第一次请求时)才会发生注入。 - Paul Samsotha
1
@PaulSamsotha 感谢你,伙计。你真的为我解决了问题,你一直是我的大帮助,继续分享知识吧。 - JayD
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接