【问题标题】:Passing Runtime (Meta)Data To Producer Method in CDI将运行时(元)数据传递给 CDI 中的生产者方法
【发布时间】:2015-12-08 14:59:51
【问题描述】:

我正在维护一个多租户应用程序,其中请求(标头、参数)的特殊元数据标识特定租户。每个租户在系统中都有自定义配置,这些配置会覆盖一些默认值。配置来自以 EJB 为前端的缓存增强型数据库。要成功查找此类自定义配置,需要密钥和租户标识符。如果租户标识符不存在,则单独使用密钥来检索密钥条目的默认值。

从接收这些请求的远程接口(servlet、web 服务等)我想检索这些标识符和设置上下文(例如将属性放在EJBContext 中),以便生产者方法可以利用设置适当的 bean 来服务每个租户的客户。在这种情况下,我还希望尽可能合理地支持 CDI 而不是 EJB。

我正在按照以下策略进行思考,但我陷入了困境。

  1. 创建一个@Config 限定符,以便CDI 容器解析到配置生产者。
  2. 创建@Key(String)配置注解,通过该注解可以获取所需配置条目的查找键。
  3. 创建一个以InjectionPoint 为参数的生产者方法。 InjectionPoint 允许获取@Key 注释,目标字段的声明类型以及声明此注入字段的类(封闭类)。如果InjectionPoint 允许我获得封闭类的一个实例,那将是一个甜蜜的场景。但想想看,这没有任何意义,因为在创建/定位和注入所有依赖项之前,实例还没有准备好。

CDI 不适合这种情况吗?如何最好地实施?

【问题讨论】:

    标签: java jakarta-ee ejb cdi


    【解决方案1】:

    一种可能的解决方案是在请求处理中提取重要的租户值,例如ServletFilter 或一些拦截器并将其存储在 ThreadLocal 持有人中。这仅在两个组件(例如过滤器和 CDI 生产者)在同一个线程中执行时才有效 - 否则您可能会遇到 EJB 问题。
    您可以在 @Produces 方法中检索租户标识符,并根据 @Key 注释值和租户 ID 返回配置条目。

    一些伪解:

    ThreadLocal 持有者

    public class ThreadLocalHolder {
    
      private static ThreadLocal<String> tenantIdThreadLocal = new ThreadLocal<>();
    
      public static String getTenantId(){
        return tenantIdThreadLocal.get();
      }
      public static void setTenantId(String tenantid){
        return tenantIdThreadLocal.set(tenantid);
      }
    }
    

    租户提取的请求过滤器

    @WebFilter(value = "/*")
    public class TenantExtractorFilter implements Filter {
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        //obtain tenant id, and store in threadlocal
        ThreadLocalHolder.setTenantId(req.getHeader("X-TENANT"));
        chain.doFilter(request, response);
      }
    }
    

    配置入口生产者

    public class Producer {
    
      //get a hold of some DAO or other repository of you config
      private ConfigRepository configRepo;
    
      @Produces
      @Config
      public String produceConfigEntry(InjectionPoint ctx) {
        Key anno = //get value of @Key annotation from the injection point, bean, property...
        String tenantId = ThreadLocalHolder.getTenantId();
        // adjust to your needs
        return configRepo.getConfigValueForTenant(anno.value(), tenantId);
      }
    }
    

    如果ThreadLocal 不是一个选项,请查看javax.transaction.TransactionSynchronizationRegistry - 无论线程池如何都有效,但显然需要事务存在。

    更新 14.12.2015
    使用请求范围 bean 作为数据持有者的替代方法

    RequestScoped 持有者

    @RequestScoped
    public class RequestDataHolder {
      private String tenantId;
    
      public String getTenantId() {
        return this.tenantId;
      }
    
      public void setTenantId(String tenantId) {
        this.tenantId = tenantId;
      }
    }
    

    网络过滤器

    从请求中提取值并将它们存储在我们的持有者中。

    @WebFilter(value = "/*")
    public class TenantExtractorFilter implements Filter {
    
      @Inject private RequestDataHolder holder;
    
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        //obtain tenant id, and store in threadlocal
        holder.setTenantId(req.getHeader("X-TENANT"));
        chain.doFilter(request, response);
      }
    }
    

    CDI 生产者 使用数据持有者并产生注入点的期望值。

    public class Producer {
    
      //get a hold of some DAO or other repository of you config
      private ConfigRepository configRepo;
      @Inject
      private RequestDataHolder dataHolder; 
    
      @Produces
      @Config
      public String produceConfigEntry(InjectionPoint ctx) {
        Key anno = //get value of @Key annotation from the injection point, bean, property...
        String tenantId = holder.getTenantId();
        // adjust to your needs
        return configRepo.getConfigValueForTenant(anno.value(), tenantId);
      }
    }
    

    我们的RequestDataHolder bean 可以注入任何 CDI、EJB、JAXRS 或 Servlet 组件,从而允许将变量从 WEB 上下文传递到其他上下文。

    注意:此解决方案需要根据 CDI 规范将 CDI 容器与 EJB 和 WEB 容器正确集成。

    【讨论】:

    • 感谢您的精彩回复!在produceConfigEntry 方法中,您取消了对ThreadLocal 的引用。你的意思是ThreadLocalHolderProducer 类是如何提供的? TransactionSynchronizationRegistry 我想会推动我使用 EJB 进行交易。纯CDI可以走这条路吗?同时,我将尝试您提出的策略并报告我的发现。提前致谢!
    • 是的,你是对的,应该是ThreadLocalHolder。正是这种对线程上下文的静态包装使您能够在一个线程中执行的所有组件之间共享上下文。事务不限于 EJB 上下文,所以理论上没有什么可以阻止您在纯 CDI 中使用它。这里的问题可能是 Web 层中没有事务。如果您的服务器支持它,您还可以将 @RequestScoped bean 注入到 yoir EJB 中。这样你就可以使用 CDI 作为数据持有者
    • 如果我不能保证我的支持业务组件(EJB / CDI bean)将在与服务远程接口(servlet、jax-ws/rs)的线程相同的线程上执行,那么使用TransactionSynchonizationRegistry 方法可能会更好。
    • 如果您可以从所有相关组件访问事务,那么可以。您还可以尝试使用 @RequestScoped bean 作为请求/租户相关属性的持有者的方法。 CDI 与 servlet 和 EJB 容器具有良好的集成。
    • 增量更新:ThreadLocal 方法有效。我创建了一个 JAX-RS 2.0 容器过滤器来挑选数据并设置线程本地上下文。不过,我还没有在负载下测试它(多线程和东西)。至于将ThreadLocalHolder 作为CDI bean 注入,我看不出这有什么意义,因为线程本地是静态的。至于TransactionSynchronizationRegistry,除了线程本地在负载下不工作外,我不会考虑它,因为事务开销。是的,可以有选择地关闭事务,但那将是不必要的样板。
    猜你喜欢
    • 1970-01-01
    • 2015-10-01
    • 2016-03-28
    • 1970-01-01
    • 2011-11-20
    • 2023-03-29
    • 2018-08-18
    • 2012-06-10
    • 2017-09-25
    相关资源
    最近更新 更多