Sunday, September 23, 2007

Using a Shared Context from EJBs

ContextSingletonBeanFactoryLocator and SingletonBeanFactoryLocator

The basic premise behind ContextSingletonBeanFactoryLocator is that there is a shared application context, which is shared based on a string key. Inside this application context is instantiated one or more other application contexts or bean factories. The internal application contexts are what the application code is interested in, while the external context is just the bag (for want of a better term) holding them. Consider that a dozen different instances of non-IoC configured, application glue code need to access a shared application context, which is defined in an XML definition on the classpath as
serviceLayer-applicationContext.xml
.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
    
<!--
  Service layer ApplicationContext definition for the application.
  Defines beans belonging to service layer.
-->
    
<beans>
    
  <bean id="myService" class="...">
    ...
  </bean>
    
  ...     bean definitions
</beans>
The glue code cannot just instantiate this context as an XmlApplicationContext; each such instantiation would get its own copy. Instead, the code relies on ContextSingletonBeanFactoryLocator, which will load and then cache an outer application context, holding the service layer application context above. Let's look at some code that uses the locator to get the service layer context, from which it gets a bean:
BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance();
BeanFactoryReference bfr = locator.useBeanFactory("serviceLayer-context");
BeanFactory factory = bfr.getFactory();
MyService myService = factory.getBean("myService");
bfr.release();
    
// now use myService
Let's walk through the preceding code. The call to ContextSingletonBeanFactoryLocator.getInstance() triggers the loading of an application context definition from a file that is named (by default) beanRefContext.xml. We define the contents of this file as follows: beanRefContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
    
<!-- load a hierarchy of contexts, although there is just one here -->
<beans>
    
  <bean id="servicelayer-context"
        class="org.springframework.context.support.ClassPathXmlApplicationContext">
    <constructor-arg>
      <list>
        <value>/servicelayer-applicationContext.xml</value>
      </list>
    </constructor-arg>
  </bean>
    
</beans>
As you can see, this is just a normal application context definition. All we are doing is loading one context inside another. However, if the outer context (keyed to the name beanRefContext.xml) had already been loaded at least once, the existing instance would just have been looked up and used. The locator.useBeanFactory("serviceLayer-context") method call returns the internal application context, which is asked for by name, serviceLayer-context in this case. It is returned in the form of a BeanFactoryRef object, which is just a wrapper used to ensure that the context is properly released when it's no longer needed. The method call BeanFactory factory = bfr.getFactory() actually obtains the context from the BeanFactoryRef. The code then uses the context via a normal getBean() call to get the service bean it needs, and then releases the context by calling release() on the BeanFactoryRef. Somewhat of a complicated sequence, but necessary because what is being added here is really a level of indirection so that multiple users can share one or more application context or bean factory definitions. We call this a keyed singleton because the outer context being used as a bag is shared based on a string key. When you get the BeanFactoryLocator via
ContextSingletonBeanFactoryLocator.getInstance();
it uses the default name
classpath*:beanRefContext.xml
as the resource location for the outer context definition. So all the files called beanRefContext.xml, which are available on the classpath, will be combined as XML fragments defining the outer context. This name (
classpath*:beanRefContext.xml
) is also the key by which other code will share the same context bag. But using the form:
ContextSingletonBeanFactoryLocator.getInstance();
for example:
contextSingletonBeanFactoryLocator.getInstance("contexts.xml");
or
contextSingletonBeanFactoryLocator.getInstance("classpath*:app-contexts.xml");
allows the name of the outer context definition to be changed. This allows a module to use a unique name that it knows will not conflict with another module. Note that the outer bag context may define inside it any number of bean factories or application contexts, not just one as in the previous example, and because the full power of the normal XML definition format is available, they can be defined in a hierarchy using the right constructor for ClasspathXmlApplicationContext, if that is desired. The client code just needs to ask for the right one by name with the
locator.useBeanFactory()
method call. If the contexts are marked as lazy-init="true", then effectively they will be loaded only on demand from client code. The only difference between SingletonBeanFactoryLocator and ContextSingletonBeanFactoryLocator is that the latter loads the outer bag as an application context, while the former loads it as a bean factory, using the default definition name of classpath*:beanRefFactory.xml. In practice, it makes little difference whether the outer bag is a bean factory or full-blown application context, so you may use either locator variant. It is also possible to provide an alias for a context or bean factory, so that one locator.useBeanFactory() can resolve to the same thing as another locator.useBeanFactory() with a different ID. For more information on how this works, and to get a better overall picture of these classes, please see the JavaDocs for ContextSingletonBeanFactoryLocator and SingletonBeanFactoryLocator.

Using a Shared Context from EJBs

We're now ready to find out how ContextSingletonBeanFactoryLocator may be used to access a shared context (which can also be the same shared context used by one or more web-apps) from EJBs. This turns out to be trivial. The Spring EJB base classes already use the BeanFactoryLocator interface to load the application context or bean factory to be used by the EJB. By default, they use an implementation called ContextJndiBeanFactoryLocator, which creates, an application context based on a classpath location specified via JNDI. All that is required to use ContextSingletonBeanFactoryLocator is to override the default BeanFactoryLocator. In this example from a Session Bean, this is being done by hooking into the standard Session EJB setSessionContext() method:
// see javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
public void setSessionContext(SessionContext sessionContext) {
  super.setSessionContext(sessionContext);
  setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
  setBeanFactoryLocatorKey("serviceLayer-context");
}
First, because the Spring base classes already implement this method so they may store the EJB SessionContext, super.setSessionContext() is called to maintain that functionality. Then the BeanFactoryLocator is set as an instance returned from ContextSingletonBeanFactoryLocator.getInstance(). If we didn't want to rely on the default outer bag context name of classpath*: beanRefContext.xml, we could use ContextSingletonBeanFactoryLocator.getInstance(name) instead. Finally, for the BeanFactoryLocator.useBeanFactory() method that Spring will call to get the final application context or bean factory, a key value of serviceLayer-context is specified, as in the previous examples. This name would normally be set as a String constant somewhere, so all EJBs can use the same value easily. For a Message Driven Bean, the equivalent override of the default BeanFactoryLocator needs to be done in setMessageDrivenContext().

4 comments:

Anonymous said...

There a several problems and omissions in your article. First of all, to obtain a BeanFactory from ContextSingletonBeanFactoryLocator you need to load that class with the same class loader as it was initially configured with.

Secondly, calling release() on the BeanFactoryReference object will close the ApplicationContext that is referenced. This means that is will be recreated next time you call the useBeanFactory() method with the same bean.

Overall it's best to avoid ContextSingletonBeanFactoryLocator if you can. It's there to offer a way out for EJBs. But even in that case it's not a walk in the park.

It depends when your application server loads WARs. Will it load a WAR if the creation of the ApplicationContext by ContextSingletonBeanFactoryLocator has failed? Will it load a WAR before the creation is complete.

The semantics are far from ideal.

Brian Repko said...

I've never been a fan of this solution as this uses reference counting in order to determine if the BeanFactory should close itself on shutdown and EJB Exception handling could cause one to fail to release and thus leak memory. But the idea of getting the service, releasing and then using the service after releasing - I never thought of that. Brilliant!! Thanks!

mx said...

thanks, that's very helpful. I did not know how to use spring's ejb stuff, but this makes it much more elegant. We use customised spring loader implementations to populate our ejb's with the required spring beans, but this is cool.

We do however, load our spring beans this way, and it works very well for us, especially being able to have hierarchies of bean factories.

EDH said...

Hello,

Interesting article.

I'm using the shared context approach for sharing my context between my war module and my ejb module.
More specifically I have one EAR with one ejb-jar module and one war module.
My EJBs (MDB's and SLSBs) extend from Spring's convenience classes and call as described the setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance("classpath*:**/beanRefContext.xml")) and setBeanFactoryLocatorKey("service") methods.

In my web.xml I have the correspondig parentContextKey and the locatorFactorySelector entries.

Everything works great … except in one scenario … when I deploy/(re)start my EAR when there are messages on the queue. In this situation I'm running into the problem that the container preloads the MDB and calls the setMessageDrivenContext method in which the above method calls occur. Of course, at that point in time my web module is not started yet and therefore the shared application context is not loaded/initialized yet.

As a result I get a spring exception, my context and my web module fail to load.

Am I doing something wrong ?
Is there a way around this ?

Any help, advise, workarounds would be appreciated because I'm stuck and I need to get this thing working by yesterday.

Kind regards,

EDH