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'