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.
@Config
qualifier so that the CDI container resolves to the configuration producer.@Key(String)
configuration annotation through which the lookup key of the desired configuration entry can be obtained.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?
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.