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.
LimitationsA 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