Wednesday, July 7, 2010

Load your Spring beans as plug-ins

There are different ways of loading Spring beans in a web application – viz using the dispatcher servlet or the context loader listener. A more modular and flexible approach (which also allows you to minimize coupling and dependencies in your applications) is to load the barebones of your application with the core context loaders and load the rest of your beans in a pluggable way. For example you may be interested in loading your page controllers, business tier and data access code as three separate modules. Off course you can do this and then use import in a main spring config file or a comma separate list in your web.xml where you configure the spring bean definition xmls. But what if you just wanted to drop in a jar with Spring beans and be sure they will be loaded and registered in the container. This also eases the pain of swapping an implementation. The goal is to put all your Spring beans in a particular layer, package it as a jar with one or more configuration file(with one following a special naming convention which is used to load them). You can also choose to go for feature based plugin – for example for show employee details use case you can package your view (freemarker template + javascript + css = I will show you how to do this in a later post), page controller (Spring MVC annotated controller), business service (interface + pojo implementation) and data access (interface + implementation) in a jar with configuration file. But my experience with this is if you go for feature based plugin – you can very soon land up with tons of plugins difficult to manage. For the moment lets just focus on the class that makes this pluggable design possible.

/**
* @author dhrubo
*/
package com.middlewareteam.spring.web.context;

import java.io.IOException;

import javax.servlet.ServletConfig;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.web.context.support.XmlWebApplicationContext;

/**
* @author dhrubo
*
*/
public class PluggableXmlWebApplicationContext extends XmlWebApplicationContext {

public static final String PLUGIN_CONTEXT_CONFIG_NAME = "pluginContextConfigName";

private final Logger log = LoggerFactory.getLogger(PluggableXmlWebApplicationContext.class);

@Override
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader)
throws IOException {

super.loadBeanDefinitions(reader);

log.info("Loading plugin application contexts.");
log.info("Seeking plugin application context configuration name from servlet config ");

ServletConfig config = this.getServletConfig();
String resourceName = null;
if(config != null) {

resourceName = this.getServletConfig().getInitParameter(PLUGIN_CONTEXT_CONFIG_NAME);
log.info("Configured plugin resource name : {}",resourceName);
}

if(StringUtils.isEmpty(resourceName)) {
log.info("Seeking plugin application context configuration name from servlet context/web app context.");
resourceName = this.getServletContext().getInitParameter(PLUGIN_CONTEXT_CONFIG_NAME);
}

log.info("Final plugin resouce name : {}",resourceName);

ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
try {
Resource[] configResources = resourcePatternResolver
.getResources(resourceName);


if (configResources.length != 0) {
log.info("Plugins found");
for (Resource resource : configResources) {
log.info("Resource == " + resource);

}
reader.loadBeanDefinitions(configResources);
} else {
log.info("No Plugins found.");
}
} catch (IOException e) {
throw new RuntimeException("Error loading plugin definitions",e);
}

}

}



Now this is how you should configure this class in your Spring bean loading mechanism.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="dms" version="2.5">
<display-name>oollaasweb</display-name>
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/config/log4j.xml</param-value>
</context-param>

<!-- security beans are here -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/config/spring-security.xml
</param-value>
</context-param>

<context-param>
<param-name>contextClass</param-name>
<param-value>
com.middlewareteam.spring.web.context.PluggableXmlWebApplicationContext
</param-value>
</context-param>

<context-param>
<param-name>pluginContextConfigName</param-name>
<param-value>
classpath*:pluginRootApplicationContext.xml
</param-value>
</context-param>


<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>


<!-- Root web application context -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- Spring security filters -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<servlet>
<servlet-name>staticresourceservlet</servlet-name>
<servlet-class>com.middlewareteam.spring.web.servlet.ResourceServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>

<servlet>
<servlet-name>dmsservlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/spring-web.xml</param-value>
</init-param>
<init-param>
<param-name>contextClass</param-name>
<param-value>com.middlewareteam.spring.web.context.PluggableXmlWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>pluginContextConfigName</param-name>
<param-value>classpath*:pluginApplicationContext.xml</param-value>
</init-param>

<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>staticresourceservlet</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>dmsservlet</servlet-name>
<url-pattern>/index.html</url-pattern>
</servlet-mapping>


<servlet-mapping>
<servlet-name>dmsservlet</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>


<servlet-mapping>
<servlet-name>dmsservlet</servlet-name>
<url-pattern>*.json</url-pattern>
</servlet-mapping>


<welcome-file-list>

<welcome-file>index.html</welcome-file>

</welcome-file-list>

</web-app>

Now just put your spring config xml with the name pluginRootApplicationContext.xml in the root of the jar and you are ready to roll out pluggable modules. You can use multiple configuration file here too. Just import them in the pluginRootApplicationContext.xml. Also if you use Maven 2 put the pluginRootApplicationContext.xml in the src/main/resources folder. Things will be much easier to package and deploy if you are using Maven.


There are few limitations to this model though. Since Spring does not yet support hot deployment (application context is read only at run time you cannot add beans while your app is running, you can refresh though but it is risky) you have to re-start your applications for the plugins to get listed in the Spring container. As I wrote earlier this is in line with Eclipse plugin architecture. Another limitation is we do not yet have a versioning. What this means is what if you are using application layer plugins and you have just modified a handful of classes. In that case you like that your plugin system can identify and register the new version and retire the old version during an application restart. However you can always delete the old jar and replace it with the new one till we have the version management.