Multi tenancy with Guice Custom Scopes and Jersey

后端 未结 1 1230
予麋鹿
予麋鹿 2021-01-06 05:13

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).

One thing

相关标签:
1条回答
  • 2021-01-06 05:30

    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.

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

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

    And here comes the TenancyScopeimplementation:

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

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

    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.).

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

    0 讨论(0)
提交回复
热议问题