Saturday, June 23, 2007

Sharing a spring context across multiple Webapps

Last month I gave a Core Spring training in Turkey. At the end of the course I discussed the architecture for an application that some of the participants were going to build after completing the course. This application would consist of an ear file with several war files inside, and the question came up if it was possible to define a single ApplicationContext that could be used as a shared parent to the WebApplicationContexts of all war files. This context would hold bean definitions for services, dao's and other beans that were not specific to a single web module.

Actually, Spring makes it very easy to do this, but neither the course nor the reference manual explain in detail how to use this feature from your web application. I therefore wrote a short sample app that illustrates how this works, which I will discuss here in my first blog entry.

The ContextLoader and SingletonBeanFactoryLocator classes

In a typical Spring web application, you use a ContextLoaderListener (or, if you're using a Servlet 2.2 / 2.3 container, a ContextLoaderServlet) to bootstrap a WebApplicationContext. You configure the ContextLoader used by this class through context parameters in your web.xml. If you've used this, you're probably familiar with the contextConfigLocation parameter, that let's you specify which files make up the WebApplicationContext to construct.

As it turns out, there is another parameter that you can use to get the desired functionality in a declaritive fashion: parentContextKey. Using this, you instruct the ContextLoader to use another class called ContextSingletonBeanFactoryLocator to search for a bean named by the value of parentContextKey, defined in a configuration file whose name matches a certain pattern. By default, this pattern is 'classpath*:beanRefFactory.xml', meaning all files called beanRefFactory on the classpath.
This bean must be an ApplicationContext itself, and this context will become the parent context for the WebApplicationContext created by the ContextLoader. However, if this context already exists then that one will be used and no new context will be created (hence the name SingletonBeanFactoryLocator).

Let's see what this means: first, we need a seperate jar in our ear that holds the code for the services, DAO's, etc. Inside this jar we place a beanRefFactory.xml file that holds a single bean-definition for an ApplicationContext. Typically, this will be a ClassPathXmlApplicationContext. Then that bean definition will refer to one or more 'regular' bean configuration files that contain the service beans and other stuff to be used by the code from your war files; something like this:


<!– contents of beanRefFactory.xml:
     notice that the bean id is the value specified by the parentContextKey param –>
<bean id="ear.context" class="org.springframework.context.support.ClassPathXmlApplicationContext">
    <constructor-arg>
        <list>
            <value>services-context.xml</value>
        </list>
    </constructor-arg>
</bean>

Finally, we need to make this jar available on the classpath of the war files using the Class-Path entry in the MANIFEST.MF file of each war.

Why would you want to use this?

A simpler solution is to skip the parentContextKey and to load the shared bean definition files from each war using the contextConfigLocation parameter. This way, each war will have its own instance of each shared bean. For simple stateless beans, such as typical services, this is actually a fine solution.

However, instantiating a new instance of each shared bean for each war can have several drawbacks: a common example is creating a Hibernate SessionFactory. This is often an expensive process, so to prevent your startup time getting out of hand the described solution will ensure that this is done only once. Another advantage of having a single instance of your SessionFactory is that it can safely act as a second level cache: you don't want multiple copies of that hanging around in a single application!
In general, if you have stateful beans that should really be used as a singleton (in the Spring sense, i.e. one instance per application as opposed to per JVM) you should define them in a single context accessed by the other contexts.

The sample

I've included the sample, both as an ear file and as source. In order to upload the ear, I had to give it a .zip extension: please rename the file to .ear before deploying it!. The source is actually an Eclipse workspace, so you can easily import and review it (it requires WTP and is configured for Spring IDE). All Spring jars needed are included. After you've deployed the app, go to the URLs /web1 and /web2 to see the output of a servlet in the first and the second war file. The toString() of the service will prove that the wars really use the same instance of the shared service.

More info

The best info on this feature is in Spring's excellent API documentation: have a look at the JavaDoc for the ContextLoader.loadParentContext method and the SingletonBeanFactoryLocator class. These docs contain further info on how to configure your web.xml and how to write the beanRefFactory.xml.

By Joris Kuipers

15 comments:

axelspin said...

And if I can`t deploy an ear? Like for Tomcat?

Is there a way to achive the same design concept with Spring+hibernate+Tomcat?

thnx
Alessandro

axelspin said...

Is the startup time due to the
Hibernate SessionFactory
the only problem? If it so.. I could live with it

Or there`s something else that could be wrong? I guess that in this case one should not use hibernate cache mechanism to add a common data caching layer on top of all the webapps. Am I right?

axelspin said...
This comment has been removed by the author.
springtips said...

Hi Alessandro,

thanx for your feedbacks..

1. if you're deploying your application as a single war webapp then there's nothing to share... and if you have several webapps but not involved in a single ear then there is no proper way to do so (you can do it by playing with your classloader hierarchy in the appserver but it's totally not adviced...)
2. The purpose of such context hierarchy is as you said a gain in startup time and memory usage, but it can be also sharing :
- the same hibernate sessionfactory (and thus the same underlying connection pool)
- The same interceptors (especially transaction interceptors on your DAOs)

hope it helps you...

axelspin said...

and is it possible to share

>- the same hibernate sessionfactory (and thus the same underlying connection pool)
>- The same interceptors (especially transaction interceptors on your DAOs)

among multipe web apps (portlets to be exposed throug wsrp) in Tomcat?

We could develop all portlets in a single webapp of course..but we`d like to avoid it

springtips said...

The problem here is that : multiple wars = multiple classloaders. nevermind if they include portlets or simple servlets.
Assume that you have two portlets (in a different webapp W1 and W2) the spring context of W1 is built in a different classloader than W2 and thus it is private to W1 and cannot be accessed by W2.
The only solutions in my opinion are:
1. to put the modules (jars) that will constitute your shared context in a higher classloader level (portal runtime classpath or even tomcat shared classpath)
2. most of application servers offers a way to configure your classloader hierarchy, so you can put your webapp W2 as a child of W1 classloader.
But i strongly disadvice you these solutions for obvious reasons...

Randy said...

I've found that when using Spring 1.2, you must explicitly define the pattern for the bean reference file -- the code won't use the parent context setting without it. Here's my example to include in a web.xml file:


<context-param>
<param-name>locatorFactorySelector</param-name>
<param-value>classpath*:beanRefFactory.xml</param-value>
</context-param>

Vikram Bailur said...

Hello,

We have multiple spring apps communicating with each other using web services - they are hosted on tomcat as we dont use app servers - but currently they are all on the same machine with different context paths - is it possible to reconfigure the applicationcontext.xml to communicate directly thru rmi or other means instead of using the web service layer instead ?

Currently the impl for the xfire web service is instantiated using spring bean - so we could theoretically change the config and add an rmi based impl ?

thanks,

Vikram

Anonymous said...

Hello,
I've observed the same situation as Randy, using Spring 2.0.4. How will this affect the solution you've provided?

TIA.

Anonymous said...

Trying to use the above mechanism to share a business tier context between two webapps. Currently there is one webapp which loads all the appContext.xml files into one context. It uses a session scoped user prefs bean as an audit interceptor for hibernate (and various other low level things).

If I split the context into presentation and shared-business, then the business context will no longer be able to see the session scoped stuff (context parent child relationship is one way of course).

Is there a way around this? I could recode the business tier locator to find the session bean on a thread-local, but I'd rather get spring to do this for me!

Damien Barthe said...

Hello,
first, many thanks for the tip and the explanation, it's exactly what I've searched for.
To test it I've download the EAR file and test it in Geronimo 1.1.1. And there, what a surprise, instead of the same instance of SampleService it appears that the two Webapps uses a different one oO.
After test the same thing in Glassfish and JBoss it appears that it works as intented.
So it's a Geronimo "problem".
After some searchs I think it's a classloader problem, but I can't "fix" it.
Do you have an idea?

Thx again,
Damien Barthe.

Ps : it seems there is the same problem on Jonas...

David Jencks said...

By advising to get the jar containing the spring app into the classloaders of each web-app using manifest-classpath I believe you are making the assumption that all web apps in an ear share the same classloader. This is not required by the specs, and on app servers that do not happen to share classloaders in this way each web-app will get its own copy of the spring app. An approach supported by the javaee specs would be to use the lib directory feature of javaee 5 ears.

Ales Novy said...

Unfortunately, similar mechanism is missing for EJBs in current ContextJndiBeanFactoryLocator implementation. It would be nice if it can take also "parentContextKey" parameter similar to org.springframework.web.context.ContextLoaderListener to have one shared root context among EJB JARs as well as WARs.

Anonymous said...

Your example EAR doesn't appear to work in WebLogic 10.x under JRockit 1.5.x. Web1 and Web2 give different instances. Any ideas why?

Carlo said...

The EAR and the source code aren't still available: could you re-upload the files?

Thanks.