
本文详解如何在 Quarkus 中正确实现 @TenantScoped 自定义 InjectableContext,解决同一 Bean 内多个同类型注入点共享实例的问题,确保各注入点获得独立实例。
本文详解如何在 quarkus 中正确实现 `@tenantscoped` 自定义 `injectablecontext`,解决同一 bean 内多个同类型注入点共享实例的问题,确保各注入点获得独立实例。
在 Quarkus(基于 Arc CDI 实现)中,自定义作用域(如 @TenantScoped)若需支持同一 Bean 内多个同类型注入点获得不同实例,关键前提在于:该作用域必须被声明为 CDI 正常作用域(Normal Scope) —— 即使用 @NormalScope(passivating = false) 注解,并配合客户端代理(client proxy)机制工作。否则,Arc 会将所有同类型注入点视为指向同一个 Bean 实例,导致 contextual 参数无法区分注入位置,从而复用同一缓存实例。
✅ 正确前提:声明为 Normal Scope
@NormalScope(passivating = false)
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD})
public @interface TenantScoped {
}
⚠️ 注意:若误用 @Scope(来自 javax.inject 或 jakarta.inject),则不属于 CDI 正常作用域,Arc 不会为其生成代理,InjectableContext.get() 将直接被多次调用但缺乏注入点上下文,无法区分语义——此时行为由实现者完全负责,且不兼容标准 CDI 语义。
? 核心问题根源与解决方案
你当前的 get(Contextual
- MyBean 中两个 @Inject MyTenantScopedQueue 字段均引用同一个 InjectableBean 实例;
- 因此 contextualMap.computeIfAbsent(contextual, ...) 总返回同一 tenantMap;
- 同一 tenantId 下仅创建一个实例,被两个字段共享。
✅ 正确做法:将注入点(Injection Point)纳入上下文键。虽然 Contextual
✅ 推荐实现:基于 InjectionPoint 增强键粒度
@Override
public <t> T get(Contextual<t> contextual, CreationalContext<t> creationalContext) {
String tenantId = getTenantIdFromContext(); // 确保线程安全获取(如 ThreadLocal)
// 关键:从 CreationalContext 获取 InjectionPoint(Quarkus Arc 支持)
InjectionPoint injectionPoint = getInjectionPoint(creationalContext);
// 构建唯一键:(bean + tenantId + injectionPoint identity)
Object key = new CompositeKey(contextual, tenantId, injectionPoint);
@SuppressWarnings("unchecked")
ContextInstanceHandle<t> handle = (ContextInstanceHandle<t>)
instanceCache.computeIfAbsent(key, k -> {
if (creationalContext == null) return null;
T instance = contextual.create(creationalContext);
return new ContextInstanceHandleImpl(
(InjectableBean<t>) contextual, instance, creationalContext);
});
return handle.get();
}
// 辅助方法:安全提取 InjectionPoint(Quarkus 2.13+ 可通过 CreationalContext 获取)
private InjectionPoint getInjectionPoint(CreationalContext> cc) {
try {
// Arc 内部通过 cc 的 private field 暴露 InjectionPoint(需反射,注意兼容性)
Field ipField = cc.getClass().getDeclaredField("injectionPoint");
ipField.setAccessible(true);
return (InjectionPoint) ipField.get(cc);
} catch (Exception e) {
// 回退策略:若反射失败,使用 tenantId + contextual 做弱隔离(适用于简单场景)
return null;
}
}
// 复合键确保唯一性
private static final class CompositeKey {
final Contextual> contextual;
final String tenantId;
final InjectionPoint injectionPoint;
CompositeKey(Contextual> contextual, String tenantId, InjectionPoint ip) {
this.contextual = contextual;
this.tenantId = tenantId;
this.injectionPoint = ip;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CompositeKey that = (CompositeKey) o;
return Objects.equals(contextual, that.contextual) &&
Objects.equals(tenantId, that.tenantId) &&
Objects.equals(injectionPoint, that.injectionPoint);
}
@Override
public int hashCode() {
return Objects.hash(contextual, tenantId, injectionPoint);
}
}</t></t></t></t></t></t>
? 注意事项与最佳实践
- 不要依赖 CreationalContext 是否为 null 判断创建时机:get(Contextual) 无 CreationalContext 时,应仅尝试获取已有实例(如上 get(Contextual) 方法),绝不在此处创建新实例。
-
线程安全性:getTenantIdFromContext() 必须从线程局部存储(如 ThreadLocal
)或请求上下文(如 RoutingContext)安全读取,避免跨线程污染。 - 清理与销毁:务必实现 destroy(ContextInstanceHandle>) 并在 tenantMap 中移除对应项,防止内存泄漏;建议结合 TenantContextListener 或 @PreDestroy 清理租户级资源。
- 性能考量:InjectionPoint 反射访问有开销,生产环境建议缓存其关键属性(如 member.getName() + declaringClass + qualifiers hash),而非直接存储整个对象。
✅ 验证效果
完成上述改造后,以下代码将注入两个独立生命周期、互不干扰的 MyTenantScopedQueue 实例:
@ApplicationScoped
class MyBean {
@Inject
MyTenantScopedQueue queue; // → 实例 A(绑定注入点 #1)
@Inject
MyTenantScopedQueue queueForOtherStuff; // → 实例 B(绑定注入点 #2)
void useQueues() {
queue.offer("msg1"); // 操作实例 A
queueForOtherStuff.offer("msg2"); // 操作实例 B
assert queue != queueForOtherStuff; // ✅ 断言通过
}
}
总结:Quarkus 自定义作用域要支持细粒度注入点隔离,本质是提升上下文键的区分维度——从 (Bean + Tenant) 升级为 (Bean + Tenant + InjectionPoint)。这既符合 CDI 正常作用域规范,又能满足多态注入的业务需求,是构建租户感知架构的坚实基础。











