2

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?

Sayo Oladeji
  • 741
  • 4
  • 15
  • 28

1 Answers1

1

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.

yntelectual
  • 3,028
  • 18
  • 24
  • Thanks for your wonderful response! In the method `produceConfigEntry` you dereferenced `ThreadLocal`. Did you mean `ThreadLocalHolder`? How is that made available in the `Producer` class? `TransactionSynchronizationRegistry` I guess would nudge me towards EJBs for transactions. Is it possible to go this route with pure CDI? Meanwhile, I'll play around with your proposed strategy and report my findings. Thanks in advance! – Sayo Oladeji Dec 10 '15 at 01:06
  • Yes, you are right, should have been `ThreadLocalHolder`. It is this static wrap over thread context that enables you to share context among all components executed in one thread. Transactions are not limited to EJB context, so in theory there is nothing that would prevent you from using it in pure CDI.Issue here might be absence of transaction in web layer. If your server supports it, you can also inject @RequestScoped beans into yoir EJB. That way you could use CDI as data holder – yntelectual Dec 10 '15 at 07:20
  • If I'm not guaranteed that my backing business components (EJB / CDI beans) will execute on the same thread as the one that serviced the remote interfaces (servlets, jax-ws/rs), then I'm probably better off with a `TransactionSynchonizationRegistry` approach. – Sayo Oladeji Dec 10 '15 at 16:29
  • If you can access the transaction from all involved components, then yes. You can also give a shot to the approach with @RequestScoped bean as a holder for the request/tenant related properties. CDI has good integrations with both servlet and EJB containers. – yntelectual Dec 11 '15 at 12:01
  • Incremental updates: The `ThreadLocal` approach works. I created a JAX-RS 2.0 container filter to pick out the data and set up the thread local context. I've not tested it under load (multiple threads and stuff) though. As for injecting the `ThreadLocalHolder` as a CDI bean, I don't see how that might make sense since the thread local is static. As for `TransactionSynchronizationRegistry`, except the thread local doesn't work under load, I won't consider it because of transaction overhead. Yes transactions can be selectively turned off but that would be unnecessary boilerplate. – Sayo Oladeji Dec 11 '15 at 16:35
  • You should not inject ThreadLocalHolder, the idea was to create a `@RequestScoped` bean and use it instead. Inject it into your jaxrs filter, fill it with attributes from request, then inject it to your other CDI beans to access those attributes. CDI container will take care of rest – yntelectual Dec 11 '15 at 18:46
  • Yes using a RequestScoped bean as a holder works fine. Injected into container filters (Jax-rs) and handler chains (jax-ws) to setup the appropriate context. Now what I want is the ability to reprocess DI on all ApplicationScoped beans at runtime whenever a configuration changed in the DB. Using events/observers would certainly work but I want to do this in a central location instead of creating observers in every ApplicationScoped. Any idea? Perhaps I should ask a new question. EE7/CDI is pretty good I must say. – Sayo Oladeji Dec 14 '15 at 10:59
  • Please do you mind refactoring the code samples in your answer to using a RequestScoped bean so that I can mark it as Accepted? Thanks a lot. – Sayo Oladeji Dec 14 '15 at 11:01
  • The original answer has been updated to include the `@RequestScoped` data holder as a mean of passing context info from Web Context to CDI & EJB context. – yntelectual Dec 14 '15 at 13:07