Monday, March 5, 2012

Setting up a multi tenant environment using the Spring Framework

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'

5 comments:

  1. 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?

    ReplyDelete
    Replies
    1. The simple thing to do is to keep the application contexts in a map, with the tenant as the key.
      When 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

      Delete
    2. Hi Oded Peer,

      I 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.

      Delete
    3. I did not understand your problem descrpition, did you create the context once during initialization?

      I 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>

      Delete
  2. Leo Technosoft developed its own SaaS framework called "SaaS Tenant™"

    This is also good for developing cloud base application...

    http://www.saas-tenant.com/

    ReplyDelete