Wednesday, April 11, 2007

Conditionally Defining Spring Beans

I like the Spring Framework. It is a useful toolkit of infrastructure that you would often build the equivalent of for any medium to large size application - and it is well designed and tested.

One feature missing from Spring that I would find handy is the ability to define a bean only if a particular property is defined or set to a particular value. For example:
  • If the property "enablestats" is defined then enable a JamonPerformanceMonitorInterceptor AOP bean to monitor application performance.
  • If the application is running inside Tomcat - determined by the existence of a Tomcat-specific property - then configure a datasource that registers with the Tomcat JTA transaction manager; otherwise if running inside WebLogic configure a datasource that registers with the WebLogic JTA transaction manager.
Of course instead of conditional logic we could split the bean definitions up into multiple files and connect them together for different tasks - e.g. construct one application context for Tomcat that includes an xml file for a Tomcat-specific datasource, and construct another application context that includes an xml file for a WebLogic-specific datasource - but if you only have a small number of different beans then it may not be worth constructing multiple application contexts at build time.

So what options do you have for conditionally defining beans? You could do programatic configuration like my colleague Solomon Duskis has blogged about. However, configuring a Spring-based application through XML is most common.

So given I'm restricting myself to xml configuration I thought I'de try out the Spring 2.0 Extensible XML Authoring API. This API allows you to add your own attributes to bean definitions or allow you to define beans using your own XML syntax. Using this API I got close to what I wanted, with a few limitations.

So without further suspense here is what a conditionally defined bean looks like:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:condbean="http://robertmaldon.com/springbeans/condbean"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://robertmaldon.com/springbeans/condbean http://robertmaldon.com/springbeans/condbean/condbean.xsd">

<condbean:cond test="${developmentmode}">
<bean id="industryDAO" class="robertmaldon.app.dao.HibernateIndustryDAO">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
</condbean:cond>
</beans>


What the example above does is "if the system property 'developmentmode' is set to some value (not null and not empty) then define the bean named 'industryDAO'".

As per the Extensible XML Authoring API the infrastructure to set up the above is as follows:

step 1) Authoring The Schema

robertmaldon/springbeans/condbean.xsd

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://robertmaldon.com/springbeans/condbean"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://robertmaldon.com/springbeans/condbean"
elementFormDefault="qualified"
attributeFormDefault="unqualified">

<xsd:element name="cond">
<xsd:complexType>
<xsd:sequence>
<xsd:any minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="test" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>

</xsd:schema>


step 2) Coding a NamespaceHandler


package robertmaldon.springbeans;

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

public class ConditionalBeanNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
super.registerBeanDefinitionParser("cond", new ConditionalBeanDefinitionParser());
}
}


step 3) Coding a BeanDefinitionParser


package robertmaldon.springbeans;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

public class ConditionalBeanDefinitionParser implements BeanDefinitionParser {

/** Default placeholder prefix: "${" */
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";

/** Default placeholder suffix: "}" */
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";

/**
* Parse the "cond" element and check the mandatory "test" attribute. If
* the system property named by test is null or empty (i.e. not defined)
* then return null, which is the same as not defining the bean.
*/
public BeanDefinition parse(Element element, ParserContext parserContext) {
if (DomUtils.nodeNameEquals(element, "cond")) {
String test = element.getAttribute("test");
if (!StringUtils.isEmpty(getProperty(test))) {
Element beanElement = DomUtils.getChildElementByTagName(element, "bean");
return parseAndRegisterBean(beanElement, parserContext);
}
}

return null;
}

/**
* Get the value of a named system property (it may not be defined).
*
* @param strVal The name of a system property. The property may
* optionally be surrounded in Ant/EL-style brackets. e.g. "${propertyname}"
*
* @return
*/
private String getProperty(String strVal) {
if (StringUtils.isEmpty(strVal)) {
return null;
}
if (strVal.startsWith(DEFAULT_PLACEHOLDER_PREFIX)) {
if(strVal.endsWith(DEFAULT_PLACEHOLDER_SUFFIX)) {
return System.getProperty(
strVal.substring(DEFAULT_PLACEHOLDER_PREFIX.length(),
strVal.length() - DEFAULT_PLACEHOLDER_SUFFIX.length()));
}
}
return System.getProperty(strVal);
}

private BeanDefinition parseAndRegisterBean(Element element, ParserContext parserContext) {
BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
BeanDefinitionHolder holder = delegate.parseBeanDefinitionElement(element);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, parserContext.getRegistry());

return holder.getBeanDefinition();
}
}


step 4) Register the Handler and the Schema

META-INF/spring.handlers

http\://robertmaldon.com/springbeans/condbean=robertmaldon.springbeans.ConditionalBeanNamespaceHandler


META-INF/spring.schemas

http\://robertmaldon.com/springbeans/condbean/condbean.xsd=robertmaldon/springbeans/condbean.xsd

Special note for developing in Tomcat: Spring looks for META-INF/spring.handlers and META-INF/spring.schemas on the classpath. webapp/META-INF is not on the Tomcat classpath, so you need to put these files inside a JAR or (hack warning) in the webapp/WEB-INF/classes/META-INF directory.

Limitations

A common way of configuring an application with property replacement is to use a PropertyPlaceholderConfigurer bean. Unfortunately this is a two-pass process: the first pass parses a bean definition, the second pass does property replacement. The Extensible Authoring XML API only allows you to interact with the first pass, so that means we are limited to things that are defined at bean definiton time, such as system properties.

The spring-beans-2.0.xsd forces you to define a <condbean:cond/> for single beans - you cannot put a <condbean:cond/> block around a group of beans.

The spring-beans-2.0.xsd prevents you from defining two beans with the same name in the same XML file, even if different <condbean:cond/> conditions guarantee only one of the definitions will be in force at any given time.

Future Enhancments

14 comments:

Anonymous said...

Thanks for posting this! As I struggle with this same problem I see several people on the net who are also trying to find a good solution here. It seems like a missing feature in spring (is a beanenabled="" attribute on the bean tag too difficult to implement?)

Unfortunately I really want a solution that lets me use a .properties file

Unknown said...

I've been able to achieve something akin to conditional bean definitions using the new capabilities of bean "aliases" in Spring 2.5. Basically, you can replace a bean definition with a named ALIAS. Dependent objects refer to the alias, not the actual bean. Then, at runtime, the actual bean definition gets bound to the alias, which can be controlled via a property using PropertyPlaceholderConfigurer or similar post-processor. I think the Spring doc contains an example. The trick is to use a placeholder variable for the bean binding in the ALIAS definition.

Stuart Mayes said...

I use the following to bring in a class as defined in my property file. Obviously this has limited use as all the beans have to have the same constructors

<bean id="id1" class="${nameFromPropertyFile}">
</bean>

I can't really see how the alias solution offers anything much more than this? The alias is still referencing a real bean, so you would still have to define more than one bean. I believe what we want is an conditional definition of a number or beans or a conditional import?

Robert Maldon said...

Hi Stuart, you are correct, the alias and your example is only applicable for the case where the bean must exist and you are just swapping the implementation. This, however, is a pretty common case.

My original thought was to be able to conditionally define groups of objects if needed. I might explore that idea again soon; there might be more hooks I can leverage now that Spring 3.0 is out and the annotations config package is a bit more mature.

Jonah said...

Robert, wondering if you've make any progress on this thought since Spring 3.0 GA is now available?

> My original thought was to be able to conditionally define groups of objects if needed. I might explore that idea again soon; there might be more hooks I can leverage now that Spring 3.0 is out and the annotations config package is a bit more mature.

Nick said...

I worked around this a different way.

I needed to use a property to select one of two ways to construct a particular bean. If you can delegate a particular bean that represents the choice point into two or more subsidiary contexts, you can use <import> with a property placeholder in the resource path.

In our case, this didn't work because we needed to use a property in a config file set up in a PropertyPlaceholderConfigurer bean. For us, we had to actually create a ClassPathXmlApplicationContext with the placeholder-wildcarded name, then use the resulting bean as a factory-bean to fetch the choice point bean.

Anonymous said...

Its kind of sad that this other guy copied your blog contents, editted the names a bit and re-published it again as if it was his:
http://suryachoudhury.wordpress.com/2009/12/23/conditionally-defining-spring-beans/

Robert Maldon said...

Good catch, Anonymous. It is clearly a cut-n-paste job. I'm flattered he/she kept the comments as is :)

Libor Šubčík said...

Nice post, very handy.
I changed one element in xsd to <xsd:any minOccurs="0" maxOccurs="unbounded" />
and therefore more bean definitions can be placed within the custom tag. All the beans can be registered using BeanDefinitionReaderUtils#registerBeanDefinition and ConditionalBeanDefinitionParser#parse can return null, the beans are still registered.

Hoffi said...

how would I use this approach to conditionally do a <import resource="..."> ?

currently the code is just for defining one bean, or?

Hoffi said...

instead of:

private BeanDefinition parseAndRegisterBean(Element element, ParserContext parserContext)

I'd need some
parseReferencedSpringXml(Element element, ParserContext parserContext)

where the given Element is the <import resource="..."> spring bean xml tag

beanicatch said...

I setup the conditional bean schema(condbean.xsd) in my localhost using apache HTTP server. I have META-INF\spring.handlers, META-INF\spring.schemas and following jars in my classpath :
commons-lang.jar, commons-logging.jar, spring.jar, xercesImpl.jar (I am using jdk1.4.2.19), and xml-apis.jar.

But when I execute my ant build script, I get
C:\Documents and Settings\lupadhy\Desktop\reloadable sping context wip\poc>ant -Darg0=true -Darg1=dev run
Buildfile: build.xml

compile:

jar:

run:
[java] Feb 14, 2012 4:22:05 PM org.springframework.core.CollectionFactory
[java] INFO: JDK 1.4+ collections available
[java] Feb 14, 2012 4:22:05 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
[java] INFO: Loading XML bean definitions from class path resource [conditionalContext.xml]
[java] org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate NamespaceHandler for nam
espace [http://localhost.uhc.com/springbeans/condbean]
[java] Offending resource: class path resource [conditionalContext.xml]
[java] at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:57)
[java] at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:64)
[java] at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:55)
[java] at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1144)
[java] at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1137)
[java] at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.
java:145)

My conditionalContext.xml contains:



















Please suggest if I am missing any dependency.

Unknown said...
This comment has been removed by the author.
Unknown said...

Thanks for the very informative post. I am trying to conditionally import a resource. But the catch is the 'condition' to be evaluated comes from a .properties file. So I went about implementing a BeanFactoryPostProcessor linked to my BeanDefinitionParser. If it is a single module, it works fine but when I am trying to use this module as a dependency into other modules, problem arises. It is simply unable to read the .properties file in the other modules and fails. What I am saying is, say I have defined my custom-namespace in module A. Module B hooks in module A. So, in module B when I am using something like:

<condition:if test="${test}">
<import resource="someResource"/>
</condition:if>

<import resource="otherResource"/>

The .properties file read in 'otherResource' are not read! Am guessing it has got something to do with the ParserContext.

Could you please advise on a workaround? I am going mad trying this.