Guice Custom Scopes 和 Jersey 的多租户
Multi tenancy with Guice Custom Scopes and Jersey
我正在使用 Guice for DI 开发 Jersey 的多租户应用程序(我也使用 Dropwizard,但我认为这在这里不重要)。
困扰我的一件事是我的应用程序中到处都是某种 tenancy_id
。我的大部分网址如下所示:
/:tenancy_id/some_resource/do_stuff
。因此,我的 Jersey 资源中的方法是使用 tenancy_id
调用的,并将其移交给调用其他服务等的服务。这些服务针对不同的租户进行了不同的配置。
我设法通过使用 @RequestScoped
TenancyIdProdiver
:
解决了这个问题
public class TenancyIdProvider {
@Inject
public TenancyIdProvider(HttpServletRequest request) {
this.request = request;
}
public TenancyId getTenancyId() {
// extract the tenancy id from the request
}
}
`
我的 GuiceModule 包含以下方法:
@RequestScoped
public TenancyId getTenancyId(TenancyIdProvider tenancyIdFactory) {
return tenancyIdFactory.getTenancyId();
}
public SomeTenancyService getTenancyId(TenancyId tenancyId, Configuration configuration) {
return new SomeTenancyService(configuration.getConfiguration(tenancyId));
}
所以现在我不需要担心服务的正确配置。一切都由 DI 容器处理,应用程序与租户无关,它不关心租户。
不过我的问题是:
所有这些服务和资源都是根据每个请求创建的,因为它们都具有 @RequestScoped
依赖性。这根本不可行。所以我的想法是用 guice 创建一个自定义范围。因此,每个租户都将获得自己的对象图,其中包含正确配置的所有资源和服务(但只有一次)。我按照示例 here 进行了尝试,但我非常不确定这在 Guice 的自定义范围中是否可行。从 Jersey 的角度来看,我需要在哪里输入我的自定义范围? ContainerRequestFilter
是正确的方法吗?
我终于自己弄明白了。关于 custom scopes 的 Guice 页面是一个很好的起点。不过我需要稍微调整一下。
首先我创建了一个 @TenancyScoped
注释:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ScopeAnnotation
public @interface TenancyScoped { }
然后我使用了一个请求过滤器:
@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
注释。过滤每个请求很重要,否则您的代码可能会表现得很奇怪(范围可能设置不正确)。
下面是 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 模块中的所有内容连接在一起:
@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 都有一个对象图(类似于每个会话有一个对象图)。
另请注意,TenancyScope
class 同时充当 Scope
和 TenancyId
提供者。
我正在使用 Guice for DI 开发 Jersey 的多租户应用程序(我也使用 Dropwizard,但我认为这在这里不重要)。
困扰我的一件事是我的应用程序中到处都是某种 tenancy_id
。我的大部分网址如下所示:
/:tenancy_id/some_resource/do_stuff
。因此,我的 Jersey 资源中的方法是使用 tenancy_id
调用的,并将其移交给调用其他服务等的服务。这些服务针对不同的租户进行了不同的配置。
我设法通过使用 @RequestScoped
TenancyIdProdiver
:
public class TenancyIdProvider {
@Inject
public TenancyIdProvider(HttpServletRequest request) {
this.request = request;
}
public TenancyId getTenancyId() {
// extract the tenancy id from the request
}
}
`
我的 GuiceModule 包含以下方法:
@RequestScoped
public TenancyId getTenancyId(TenancyIdProvider tenancyIdFactory) {
return tenancyIdFactory.getTenancyId();
}
public SomeTenancyService getTenancyId(TenancyId tenancyId, Configuration configuration) {
return new SomeTenancyService(configuration.getConfiguration(tenancyId));
}
所以现在我不需要担心服务的正确配置。一切都由 DI 容器处理,应用程序与租户无关,它不关心租户。
不过我的问题是:
所有这些服务和资源都是根据每个请求创建的,因为它们都具有 @RequestScoped
依赖性。这根本不可行。所以我的想法是用 guice 创建一个自定义范围。因此,每个租户都将获得自己的对象图,其中包含正确配置的所有资源和服务(但只有一次)。我按照示例 here 进行了尝试,但我非常不确定这在 Guice 的自定义范围中是否可行。从 Jersey 的角度来看,我需要在哪里输入我的自定义范围? ContainerRequestFilter
是正确的方法吗?
我终于自己弄明白了。关于 custom scopes 的 Guice 页面是一个很好的起点。不过我需要稍微调整一下。
首先我创建了一个 @TenancyScoped
注释:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ScopeAnnotation
public @interface TenancyScoped { }
然后我使用了一个请求过滤器:
@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
注释。过滤每个请求很重要,否则您的代码可能会表现得很奇怪(范围可能设置不正确)。
下面是 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 模块中的所有内容连接在一起:
@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 都有一个对象图(类似于每个会话有一个对象图)。
另请注意,TenancyScope
class 同时充当 Scope
和 TenancyId
提供者。