Search code examples
javajakarta-eeejbcdi

Passing Runtime (Meta)Data To Producer Method in CDI


I am maintaining a multi-tenant application in which special metadata on requests (headers, params) identify specific tenants. Each tenant has custom configurations in the system that override some defaults. The configurations come from a cache-augmented database fronted by an EJB. To successfully look up one such custom configuration, a key and a tenant identifier is needed. If the tenant identifier is not present, the key alone is used to retrieve the default for the key's entry.

From the remote interfaces that receive these requests (servlets, web services, etc) I want to retrieve such identifiers and setup contexts (e.g put properties in EJBContext) with them such that producer methods can leverage to setup appropriate beans to service each tenant's clients. I would also ideally want to favor CDI over EJBs for this case as much as reasonable.

I was thinking along the lines of the following strategy but I got stuck.

  1. Create a @Config qualifier so that the CDI container resolves to the configuration producer.
  2. Create a @Key(String) configuration annotation through which the lookup key of the desired configuration entry can be obtained.
  3. Create a Producer method which takes an InjectionPoint as a parameter. The InjectionPoint allows to obtain the @Key annotation, the declared type of the Field being targeted and the class in which this injected field is declared (enclosing class). A sweet scenario would be if InjectionPoint allows me to obtain an instance of the the enclosing class. But thinking of it, this doesn't make sense as the instance wouldn't be ready yet until all it's dependencies have been created/located and injected.

Is this a case CDI is not meant for? How could this best be implemented?


Solution

  • One possible solution is to extract the significant tenant values in request processing e.g. ServletFilter or some Interceptor and store it in a ThreadLocal holder. This will only work if both components(e.q. filter and CDI producer) are executed in the same thread - therfore you might run into issues with EJBs.
    You can retrieve the tenant identifier in your @Produces method and return the config entry based on the @Key annotation value and tenant id.

    Some pseudo solution:

    ThreadLocal holder

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

    Request filter for tenant extraction

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

    Config entry producer

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

    If ThreadLocal is not an option, have a look at javax.transaction.TransactionSynchronizationRegistry - which works regardless of thread pools, but requires a transaction presence obviously.

    Update 14.12.2015
    Alternative approach using request scoped bean as data holder

    RequestScoped holder

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

    WebFilter

    Extracts the values from request and stores them in our holder.

    @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 producer Uses the data holder and produces the expected value forinjection point.

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

    Our RequestDataHolder bean can be injected into any CDI, EJB, JAXRS or Servlet component thereby allowing to pass variable from WEB context to other contexts.

    Note: this solution requires proper integration of CDI container with EJB and WEB containers according to CDI spec.