In this post I'll describe how to set up a multi tenant application using the Spring Framework (I am using Spring 3).
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
The solution:
- Create a Spring ApplicationContext for every tenant
- Set the PropertyConfigurer in runtime
Let's see this in action:
public class MultiTenantSpringExample {
private ClassPathXmlApplicationContext commonCtx; // use ClassPathXmlApplicationContext instead of ApplicationContext so we can destory them
private List tenantContexts;
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'