使用 Guice Custom Scopes 和 Jersey 进行多租户租户、Custom、Guice、Jersey

2023-09-06 15:11:34 作者:拿根辣条砸死你

我正在使用 Guice for DI 与 Jersey 开发多租户应用程序(我也使用 Dropwizard,但我认为这并不重要).

I am in the process of developing a multi tenancy application with Jersey using Guice for DI (I also use Dropwizard but I don't think it matters here).

困扰我的一件事是,某种 tenancy_id 在我的应用程序中随处可见.我的大多数 URL 如下所示:/:tenancy_id/some_resource/do_stuff.因此,我的 Jersey 资源中的方法使用 tenancy_id 调用,并将其交给调用其他服务的服务等等.这些服务针对不同的租户进行不同的配置.

One thing that bothers me is the fact that some kind of tenancy_id is all over the place in my application. Most of my URLs look like this: /:tenancy_id/some_resource/do_stuff. So the method in my Jersey resource is called with the tenancy_id and hands it over to a service which calls other services and so on. These services are configured differently for different tenants.

我设法通过使用 @RequestScoped TenancyIdProdiver 解决了这个问题:

I managed to resolve this problem by using a @RequestScoped TenancyIdProdiver:

public class TenancyIdProvider {

    @Inject
    public TenancyIdProvider(HttpServletRequest request) {
        this.request = request;
    }

    public TenancyId getTenancyId() {
        // extract the tenancy id from the request
    }
}

`

我的 GuiceModule 包含以下方法:

My GuiceModule contains the following methods:

@RequestScoped 
public TenancyId getTenancyId(TenancyIdProvider tenancyIdFactory) { 
    return tenancyIdFactory.getTenancyId(); 
}

public SomeTenancyService getTenancyId(TenancyId tenancyId, Configuration configuration) { 
    return new SomeTenancyService(configuration.getConfiguration(tenancyId)); 
}

所以现在我无需担心服务的正确配置.一切都由 DI 容器处理,应用程序与租户无关,它不关心租户.

So now I don't need to worry about proper configuration of my services. All is handled by the DI container and the application is tenant agnostic where it doesn't care about the tenant.

我的问题是:所有这些服务和资源都是在每个请求上创建的,因为它们都具有 @RequestScoped 依赖项.这根本不可行.所以我的想法是用 guice 创建一个自定义范围.因此,每个租户都将获得自己的对象图,其中所有资源和服务都已正确配置(但只有一次).我按照 here 的示例进行了尝试,但我不确定这是否可能Guice 的自定义范围.从泽西岛的角度来看,我需要在哪里输入我的自定义范围?ContainerRequestFilter 是正确的方法吗?

My question is though: All these services and resources are created on every single request, since they all have a @RequestScoped dependency. This is not feasible at all. So my idea was to create a custom scope with guice. So every tenant will get its own object graph with all resources and services properly configured (but only once). I tried it following the example here, but I am very unsure if this is even possible with Guice' custom scopes. Where do I need to enter my custom scope from a Jersey point of view? Is a ContainerRequestFilter the right way to do it?

推荐答案

我终于自己弄明白了.关于 自定义范围 的 Guice 页面是一个很好的起点.不过我需要稍微调整一下.

I finally figured it out by myself. The Guice page about custom scopes was a good starting point. I needed to tweak it a bit though.

首先我创建了一个 @TenancyScoped 注释:

First I've created a @TenancyScoped annotation:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ScopeAnnotation
public @interface TenancyScoped { }

然后我使用了请求过滤器:

Then I used a request filter:

@PreMatching
public class TenancyScopeRequestFilter implements ContainerRequestFilter {

   private final TenancyScope      scope;

   @Inject
   public TenancyScopeRequestFilter(TenancyScope scope) {
      this.scope = scope;
   }

   @Override
   public void filter(ContainerRequestContext requestContext) throws IOException {
      Optional<TenancyId> tenancyId = getTenancyId(requestContext);

      if (!tenancyId.isPresent()) {
         scope.exit();
         return;
      }
      scope.enter(tenancyId.get());
   }

   private Optional<TenancyId> getTenancyId(ContainerRequestContext requestContext) {
   }
}

请注意 @PreMatching 注释.过滤每个请求很重要,否则您的代码可能会表现得很奇怪(范围可能设置不正确).

Please note the @PreMatching annotation. It is important that every request is filtered, otherwise your code might behave weirdly (scope could be set incorrectly).

TenancyScope实现来了:

 public class TenancyScope implements Scope, Provider<TenancyId> {

     private final Logger                                        logger             = LoggerFactory.getLogger(TenancyScope.class);

     private final ThreadLocal<Map<TenancyId, Map<Key<?>, Object>>> tenancyIdScopedValues = new ThreadLocal<>();
     private final ThreadLocal<TenancyId>                           tenancyId             = new ThreadLocal<>();

     public void enter(TenancyId tenancyId) {
        logger.debug("Enter scope with tenancy id {}", tenancyId);

        if (this.tenancyIdScopedValues.get() == null) {
           this.tenancyIdScopedValues.set(new HashMap<>());
        }

        this.tenancyId.set(tenancyId);
        Map<Key<?>, Object> values = new HashMap<>();
        values.put(Key.get(TenancyId.class), tenancyId);
        this.tenancyIdScopedValues.get().putIfAbsent(tenancyId, values);
     }

     public void exit() {
        logger.debug("Exit scope with tenancy id {}", tenancyId.get());

        this.tenancyId.set(null);
     }

     public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
        return new Provider<T>() {
           public T get() {
              logger.debug("Resolve object with key {}", key);
              Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);

              @SuppressWarnings("unchecked")
              T current = (T) scopedObjects.get(key);
              if (current == null && !scopedObjects.containsKey(key)) {
                 logger.debug("First time instance with key {} is in tenancy id scope {}", key, tenancyId.get());
                 current = unscoped.get();

                 // don't remember proxies; these exist only to serve circular dependencies
                 if (Scopes.isCircularProxy(current)) {
                    return current;
                 }
                 logger.debug("Remember instance with key {} in tenancy id scope {}", key, tenancyId.get());
                 scopedObjects.put(key, current);
              }
              return current;
           }
        };
     }

     private <T> Map<Key<?>, Object> getScopedObjectMap(Key<T> key) {
        Map<TenancyId, Map<Key<?>, Object>> values = this.tenancyIdScopedValues.get();
        if (values == null || tenancyId.get() == null) {
           throw new OutOfScopeException("Cannot access " + key + " outside of a scoping block with id " + tenancyId.get());
        }
        return values.get(tenancyId.get());
     }

     @Override
     public TenancyId get() {
        if (tenancyId.get() == null) {
           throw new OutOfScopeException("Cannot access tenancy id outside of a scoping block");
        }
        return tenancyId.get();
     }

  }

最后一步是将 Guice 模块中的所有内容连接在一起:

The last step is to wire everything together in the Guice module:

@Override
protected void configure() {
   TenancyScope tenancyScope = new TenancyScope();
   bindScope(TenancyScoped.class, tenancyScope);
   bind(TenancyScope.class).toInstance(tenancyScope);
   bind(TenancyId.class).toProvider(tenancyScope).in(TenancyScoped.class);
}

您现在拥有的是在每个请求之前设置的范围,并且 Guice 提供的所有实例都按租户 ID 缓存(也按线程,但可以轻松更改).基本上,每个租户 id 都有一个对象图(类似于每个会话有一个,例如).

What you have now is a scope that is set before each request and all instances the are provided by Guice are cached per tenancy id (also per thread, but that can be changed easily). Basically you have a object graph per tenant id (similar to having one per session e.g.).

另外请注意,TenancyScope 类同时充当 ScopeTenancyId 提供者.

Also notice, that the TenancyScope class acts both as a Scope and a TenancyId provider.