Sunday, November 18, 2007

Controlling Classloading with Spring

The Spring Framework provides tons of functionality for someone wanting to use dependency injection; so much so that it can be difficult to know everything you can do.

Many times when dealing with a framework or a web application, it becomes important to track down JAR files and class files at runtime, and load them, often times in their own ClassLoader object.

Just to remind everyone out there, classes loaded in one ClassLoader are not visible to a parent or sibling ClassLoader (unless you are using some nightmare ClassLoader object that breaks the hierarchical semantics of core Java classloading).

ClassLoaderA
| (ClassA, ClassB, ClassC)
|
|__ ClassLoaderB (Child of A)
|   (ClassD, ClassE)
|
|__ ClassLoaderC (Child of A)
   (ClassF, ClassG)

Using this diagram as an example, ClassLoaderB can see ClassA, ClassB, ClassC, ClassD, and ClassE, but it cannot see ClassF or ClassG. Likewise, ClassLoaderC cannot see the classes loaded by ClassLoaderB. And finally, ClassLoaderA can only see it's own classes.

Thankfully, objects loaded by child/sibling classloaders can be dealt with in classes loaded by parent/peer classloaders (this can be seen when you put one of your objects in to an ArrayList object - the ArrayList may not be able to reference your class, but it can still deal with the objects inside of that class.

Considering the above, what if we want to use a Spring XMLBeanFactory object (created by ClassLoaderA) to load a bean definition in an XML file that is referring to ClassD which is inside ClassLoaderB?

Thankfully, it is a fairly simple process; it simply involves understanding the two pieces at work. We have:

  1. A Bean Definition Reader - This is the object that reads the bean definition from the source (redundant, aren't I?); in other words, in this case this is the XML parser and magical finder of Class objects when you type ....
  2. A Bean Factory - This is the implementation of the factory that handles the construction of beans after they have been defined by the reader. This is the stage in the process where AOP, autowiring, property setting, singletons, prototypes, etc is all plugged in.
So now, being infinitely more educated on the Spring bean factories, it is just a short hop to plugging in the correct class loader ClassLoaderB for the task at hand.

The BeanDefinitionReader is where classes are found when Spring is parsing/interpreting a bean definition source (such as a bean XML file). There is a method on the interface - BeanDefinitionReader.getBeanClassLoader() that provides the correct class loader to the BeanFactory (this defaults to Thread.currentThread().getContextClassLoader()). There is also a method on the abstract base implementation - AbstractBeanDefinitionReader.setBeanClassLoader(ClassLoader) so we can set our class loader to the correct implementation.

'But wait R.J.!', you say - 'There is no BeanDefinitionReader that I can see on the XmlBeanFactory class - you're a liar! I'm never reading Coffee-Bytes again!'. Don't pull away from me yet! While it is true that the XmlBeanFactory class doesn't provide visibility to the bean definition reader, that doesn't mean it's not there! In reality, the XmlBeanFactory is just a convenience implementation of two seperate objects:

XmlBeanFactory factory = new XmlBeanFactory([xml resource here!]);
// == to
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader();
DefaultListableBeanFactory factory = new DefaultListableBeanFactory(reader);
reader.loadBeanDefinitions([xml resource here!]);

Do I think it is frustrating that you can't get to the bean definition reader without forgoing the XmlBeanFactory class for it's super class? Absolutely! Do I have commit rights to Spring? Unfortunately for them, no (hint, hint). So, that's that... oh, you still don't feel that sweet cut/paste solution beckoning to you? Am I leaving you patient readers hanging without a clean solution to your problems? Ok, ok...

XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader();
reader.setBeanClassLoader([Class LoaderB here!]);
DefaultListableBeanFactory factory = new DefaultListableBeanFactory(reader);
reader.loadBeanDefinitions([xml resource here!]);

No comments: