Singleton Container Access, Good or Evil?


One of the primary goals of Spring as a container is to eliminate the typical singletons and ad hoc factories that most applications end up using for access to objects.

Important 

Based on experience, we feel that in a properly designed and configured application written in IoC style and running in a sophisticated IoC container such as Spring, almost no application code needs to know about the container, or resort to singletons for access to other code.

That said, there is no question that in a number of applications, a small amount of glue code that is container aware is often needed to kick things off at some point of execution, typically obtaining one or more configured objects from the Spring container and starting the execution of a chain of related actions. This glue code may come from Spring itself (as in the case of the request handler mechanism in the Spring Web MVC layer), or in the form of application code.

One of the main cases where application code may need to be aware of the Spring container is when the Spring container is not itself responsible for creating objects that then need to work with other objects from the Spring container. In the better (ideal) variant of this scenario, the other entity creating objects can at least be made to pass along an instance of the Spring container to the newly created object. For example, in Spring's Quartz scheduler integration, the scheduler and Quartz jobs are configured in the application context. While it is the Quartz scheduler, and not Spring itself, that actually creates new Jobs, it is easy to at least pass a reference to the application context, as part of the Job data, to the newly created job. So the Job does have to work with the container but doesn't have to worry about how to get the container.

However, consider the case of EJBs, which are created by the EJB container. There is simply no way to force the EJB container to somehow provide a newly created EJB with a reference to an existing Spring application context or bean factory. One option is for each EJB instance to create its own application context instance, and this is the default behavior of the Spring EJB base classes, as described previously. However, this is often not going to be an ideal solution. It is problematic when there are resources in the Spring container that have a relatively expensive (in terms of time) startup cost. Consider for example a Hibernate SessionFactory, which has to enumerate and initialize a number of class mappings. It is also problematic when the resources in the Spring container start using up significant amounts of memory. While EJBs are pooled by the container so Spring containers would not be continuously created in normal usage, it's clear that a solution for shared usage of a Spring container is needed.

Any scenario that has the same constraints as EJBs can also benefit from shared access to a Spring container. Spring provides a generic bean factory–accessing interface called BeanFactoryLocator:

 public interface BeanFactoryLocator {        /**    * Use the BeanFactory (or derived class such as ApplicationContext) specified    * by the factoryKey parameter. The definition is possibly loaded/created as    * needed.    * @param factoryKey a resource name specifying which BeanFactory the     * BeanFactoryLocator should return for usage. The actual meaning of the resource    * name is specific to the actual implementation of BeanFactoryLocator.    * @return the BeanFactory instance, wrapped as a BeanFactoryReference object    * @throws BeansException if there is an error loading or accessing the    * BeanFactory    */   BeanFactoryReference useBeanFactory(String factoryKey) throws BeansException;      } 

BeanFactoryLocator in and of itself does not imply singleton access to anything, but Spring does include a couple of almost identical “keyed” singleton implementations of BeanFactoryLocator, called ContextSingletonBeanFactoryLocator and SingletonBeanFactoryLocator.

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  >     ...   </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          >     <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(<resource name>); 

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(<id>) 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(<id>) can resolve to the same thing as another locator.useBeanFactory(<id>) 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.

A Shared Context as the Parent of a Web-App Application Context

In Chapter 4, we examined how Spring's ContextLoader class, triggered by the ContextLoaderListener or ContextLoaderServlet, can be used to load an application context for the web-app. You may want to review that section of Chapter 4 at this time.

Especially when creating a J2EE application, with multiple web-apps (and possibly EJBs), it is often desirable to define a shared parent application context to one or more web-app application contexts. In this setup, all service layer code can move into the shared context, with the web-app contexts having only bean definitions appropriate to the actual web view layer. Note that this does potentially affect how you will package your Spring-based app because the Spring framework and all classes that are used across web-apps will have to live in a classloader shared by the web applications, such as any global EJB classloader or an application server classloader. Most J2EE appservers do support a number of class-loader setup variations, allowing this configuration to work.

It is relatively trivial to subclass the existing ContextLoader class, so that using ContextSingletonBeanFactoryLocator, it triggers the loading of a shared parent context (with any other web-app inside the same J2EE app, which is configured the same).

Note 

Note that the following customization of ContextLoader has actually made it into Spring itself, for version 1.1.4. Before using this customization, ensure that it is actually needed in your version of Spring.

Let's look at the necessary code to customize ContextLoader:

public class SharedParentLoadingContextLoader         extends org.springframework.web.context.ContextLoader {       // --- statics       protected static final Log log = LogFactory.getLog(ContextLoader.class);       /** servlet param specifying locator factory selector */   public static final String LOCATOR_FACTORY_SELECTOR = "locatorFactorySelector";   /** servlet param specifying the key to look up parent context from locator */   public static final String BEAN_FACTORY_LOCATOR_FACTORY_KEY =            "parentContextKey";       // --- attributes       protected BeanFactoryReference _beanFactoryRef = null;       /**    * Overrides method from superclass to implement loading of parent context    */   protected ApplicationContext loadParentContext(ServletContext servletContext)           throws BeansException {         ApplicationContext parentContext = null;         String locatorFactorySelector = servletContext             .getInitParameter(LOCATOR_FACTORY_SELECTOR);     String parentContextKey = servletContext             .getInitParameter(BEAN_FACTORY_LOCATOR_FACTORY_KEY);         try {       if (locatorFactorySelector != null) {         BeanFactoryLocator bfr = ContextSingletonBeanFactoryLocator                 .getInstance(locatorFactorySelector);             log.info("Getting parent context definition: using parent context key of '"                  + parentContextKey + "' with BeanFactoryLocator");         _beanFactoryRef = bfr.useBeanFactory(parentContextKey);         parentContext = (ApplicationContext) _beanFactoryRef.getFactory();       }     }     catch (BeansException ex) {       throw ex;     }         return parentContext;   }       /**    * Close Spring’s web application definition for    *     * @param servletContext    *            current servlet definition    */   public void closeContext(ServletContext servletContext)           throws ApplicationContextException {     servletContext.log("Closing root WebApplicationContext");         WebApplicationContext wac = WebApplicationContextUtils             .getRequiredWebApplicationContext(servletContext);     ApplicationContext parent = wac.getParent();     try {       if (wac instanceof ConfigurableApplicationContext) {         ((ConfigurableApplicationContext) wac).close();       }     }     finally {       if (parent != null && _beanFactoryRef != null)         _beanFactoryRef.release();     }   } }

The normal ContextLoader implementation already provides template methods, which subclasses may use to load a parent context to the web-app context, so all we are doing here is hooking into those methods to load the shared parent via ContextSingletonBeanFactoryLocator.

We also need a specialized version of ContextLoaderListener to call our variant of ContextLoader:

 public class ContextLoaderListener         extends org.springframework.web.context.ContextLoaderListener {   protected org.springframework.web.context.ContextLoader createContextLoader() {     return new SharedParentLoadingContextLoader();   } } 

Finally, we modify the normal web-app web.xml configuration file to add parameters for the parent context:

<web-app>      <context-param>          <param-name>locatorFactorySelector</param-name>          <param-value>classpath*:beanRefContext.xml</param-value>  </context-param>  <context-param>          <param-name>parentContextKey</param-name>          <param-value>servicelayer-context</param-value>  </context-param>      ...     </web-app>

For the ContextSingletonBeanFactoryLocator.getInstance(String selector) method call, we are specifying a value of classpath*:beanRefContext.xml. We are also specifying that inside of the context bag defined by beanRefContext.xml, we are interested in using the context with the ID of servicelayer-context.

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, as described in a previous section, 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().



Professional Java Development with the Spring Framework
Professional Java Development with the Spring Framework
ISBN: 0764574833
EAN: 2147483647
Year: 2003
Pages: 188

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net