I'll use a pure Java application, but the concepts work well in enterprise apps.
The requirements are simple:
- Single application to serve all the tenants
- Every tenant uses the same wiring, each tenant has different properties
- Have common properties and wiring that can be shared by all the tenants
- Allow each tenant to override the common settings
- Create a Spring ApplicationContext for every tenant
- Set the PropertyConfigurer in runtime
public class MultiTenantSpringExample { private ClassPathXmlApplicationContext commonCtx; // use ClassPathXmlApplicationContext instead of ApplicationContext so we can destory them private ListtenantContexts; public void init( List tenants ) { tenantContexts = new ArrayList ( tenants.size() ); // create a common application context, shared among all the tenants commonCtx = new ClassPathXmlApplicationContext( "/commonContext.xml" ); // set up all the tenants for ( String tenant : tenants ) { // for each tenant create a Spring ApplicationContext ClassPathXmlApplicationContext tenantCtx = new ClassPathXmlApplicationContext(); tenantCtx.setParent( commonCtx ); tenantCtx.setConfigLocation( "/tenantContext.xml" ); TenantPropertyPlaceholderConfigurer beanFactoryPostProcessor = new TenantPropertyPlaceholderConfigurer( tenant ); tenantCtx.addBeanFactoryPostProcessor( beanFactoryPostProcessor ); tenantCtx.refresh(); tenantContexts.add( tenantCtx ); } } public void destroy() { // destroy the tenant contexts for ( ClassPathXmlApplicationContext tenantContext : tenantContexts ) { tenantContext.destroy(); } tenantContexts.clear(); // destroy the common context commonCtx.destroy(); } public static void main( String[] args ) { MultiTenantSpringExample example = new MultiTenantSpringExample(); example.init( Arrays.asList( "a", "b" ) ); example.destroy(); } }
On line 18 I create the common wiring, that is shared among tenants, and set it as the parent for the tenant application context on line 23.
On line 25-27 I inject the PropertyPlaceholderConfigurer, which is created differently for every tenant.
public class TenantPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer { public TenantPropertyPlaceholderConfigurer( String tenant ) { super(); setIgnoreResourceNotFound( true ); // this makes the common file and tenant file optional // prepare the default properties String defaultPropertiesResourcePath = "/global.properties"; Resource defaultPropertiesResource = new ClassPathResource( defaultPropertiesResourcePath ); // prepare tenant properties String tenantPropertiesResourcePath = '/' + tenant + ".properties"; Resource tenantPropertiesResource = new ClassPathResource( tenantPropertiesResourcePath ); // set the locations Resource[] locations = new Resource[] { defaultPropertiesResource, tenantPropertiesResource }; setLocations( locations ); } }
The TenantPropertyPlaceholderConfigurer uses classpath resources, using a common properties file, shared for all the tenants, and a per-tenant properties file.
To complete the example I created a simple class, A, holding an int, and printing the int in the print() method.
public class A { private int i; public void setI( int i ) { this.i = i; } public int getI() { return i; } public void print() { System.out.println( "Example property: " + i ); } }
And the resource files: commonContext.xml, empty in the example but can be used for sharing wiring among tenants
<beans> </beans>
tenantContext.xml, creates an instance of A for every tenant, each one with different property values, and calls the print() method after constructing the object to print the value.
<beans> <bean class="A" id="a" init-method="print"> <property name="i" value="${tenant-i}"> </property></bean> </beans>
And two matching properties files: a.properties
tenant-i = 1
b.properties
tenant-i = 2
When executing the 'main' method the program outputs "1" for tenant 'a' and "2" for tenant 'b'
And how do I use your a-bean? Let's say that I (auto)wired it some where, how does the tenant instance get resolved?
ReplyDeleteThe simple thing to do is to keep the application contexts in a map, with the tenant as the key.
DeleteWhen your application serves a request it must already know which tenant issued the request, thus looking up the tenant's application context is easy.
Let's compare this to what Spring provides for web applications. You define your Spring XML configuration, and set up and access beans in your web application by using ContextLoaderListener and WebApplicationContextUtils.
In a multi-tenant appplication I would implement javax.servlet.ServletContextListener by myslef (not relying on Spring's ContextLoaderListener), and provide a static getter on that class, which gets the tenant as a parameter and returns the tenant's application context (very similar to what you do with Spring's WebApplicationContextUtils).
In both ways, once you have a reference to the application context you can get any bean you defined in your configuration.
For more information on what Spring provides to web applications see Spring documentation chapter 17.2
Hi Oded Peer,
DeleteI followed your post and I am able to get the spring context based on the tenant information. But the issue is, for each request from the tenant, how to use the existing context for that tenant. In my scenario, for each request context is get refreshing.
Please provide some help on this....
Thank You.
I did not understand your problem descrpition, did you create the context once during initialization?
DeleteI created a simple example using the files above, does this helps you?
package test;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.support.ClassPathXmlApplicationContext;
@SuppressWarnings("serial")
@WebServlet(value = "/*", loadOnStartup = 1, initParams = { @WebInitParam(name = "tenants", value = "a,b") })
public class MyServlet extends HttpServlet {
private static Map<String, ClassPathXmlApplicationContext> tenantContexts;
@Override
public void init() throws ServletException {
super.init();
tenantContexts = new HashMap<String, ClassPathXmlApplicationContext>();
String[] tenants = getInitParameter( "tenants" ).split( " *, *" );
// set up all the tenants
for ( String tenant : tenants ) {
// for each tenant create a Spring ApplicationContext
ClassPathXmlApplicationContext tenantCtx = new ClassPathXmlApplicationContext();
tenantCtx.setConfigLocation( "/beans.xml" );
TenantPropertyPlaceholderConfigurer beanFactoryPostProcessor = new TenantPropertyPlaceholderConfigurer( tenant );
tenantCtx.addBeanFactoryPostProcessor( beanFactoryPostProcessor );
tenantCtx.refresh();
tenantContexts.put( tenant, tenantCtx );
}
}
@Override
public void destroy() {
super.destroy();
for ( ClassPathXmlApplicationContext tenantContext : tenantContexts.values() ) {
tenantContext.destroy();
}
tenantContexts.clear();
}
@Override
protected void doGet( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException {
String tenant = req.getParameter( "tenant" );
A a = (A)tenantContexts.get( tenant ).getBean( "a" );
resp.getWriter().print( "<html><body><h1>i: " + a.getI() + "</h1>" + "<br><h2>tenant: " + tenant + "</body></html>" );
}
}
</pre>
Leo Technosoft developed its own SaaS framework called "SaaS Tenant™"
ReplyDeleteThis is also good for developing cloud base application...
http://www.saas-tenant.com/