Suryachoudhury’s Weblog

December 23, 2009

Conditionally Defining Spring Beans

Filed under: Uncategorized — Surya Choudhury @ 9:05 pm

One feature missing from Spring framework that I would find handy is the ability to define a bean based on an EL (Expressional Language) or a particular property is defined.

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


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:if="http://mycompany.com/springbeans/condition"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
   http://mycompany.com/springbeans/condition http://mycompany.com/springbeans/condition/condition.xsd">

<!– Please note the name space “xmlns:if=”http://mycompany.com/springbeans/condition” and schema
location “*http://mycompany.com/springbeans/conditionhttp://mycompany.com/springbeans/condition/condition.xsd*
–>
<if:condition
test=”${(jms.server.type == ‘activemq’) &amp;&amp; (isMessageBrokerEnabled == true)}”
varnames=”jms.server.type,isMessageBrokerEnabled”
src=”META-INF/bootstrap.properties” >

<bean id="apacheMQConnectionFactory">
    <property name="brokerURL" value="tcp://e3_cloud_senxex.mycompany.com:61616"/>
    <property name="userName" value="admin"/>
    <property name="password" value="xVkE2iPpk9"/>
</bean>
</if:condition>

What the example above does? Is a bean defination defining a Sping JMS connector for Apache ActiveMQ. If the EL condition is *true* then the bean will be instantiated.

The property jms.server.type and isMessageBrokerEnabled is defined in the property file bootstrap.properties

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 Apache ActiveMQ and Sun Message Broker that includes an xml file for a JMS provider specific configurations,  but if you only have a small number of different beans then it may not be worth constructing multiple application contexts at build time. Even for products that comesup with multiple JMS connector support, where installer are provided to the product sells/service team for customer side installation. In such senerio we need to make installation process simple and we don’t want to individual build for each customer.

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.

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

Step 1) Authoring The Schema

com/mycompany/product/spring/condition.xsd

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


<xsd:schema xmlns="http://mycompany.com/springbeans/condition"

            xmlns:xsd="http://www.w3.org/2001/XMLSchema"


            xmlns:beans="http://www.springframework.org/schema/beans"


            targetNamespace="http://mycompany.com/springbeans/condition"


            elementFormDefault="qualified"

            attributeFormDefault="unqualified">  
 
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />  
 
<xsd:annotation> 
 
<xsd:documentation><![CDATA[ 
 
       Defines the configuration elements for My Comany Spring Framework's conditional bean creation. 
 
       Limitations: 
 
       1. The spring-beans-2.0.xsd forces you to define a <if:condition/>


          for single beans - you cannot put a <if:condition/> block around a group of beans. 
 
       2. The spring-beans-2.0.xsd prevents you from defining two beans with the same name 
 
          in the same XML file, even if different <if:condition/> conditions guarantee only
          one of the definitions will be in force at any given time. ]]>
</xsd:documentation> 
</xsd:annotation>
 
<xsd:element name="condition">
   <xsd:complexType>
      <xsd:sequence>
        <xsd:any minOccurs="0" />
      </xsd:sequence> 
      <xsd:attribute name="test" type="xsd:string" use="required"> 
 
       <xsd:annotation>


          <xsd:documentation><![CDATA[


                   Define the param value that need to tested as a condition for bean creation. 
                   For example if '${myCondition}=true' then the following child bean will be instantiated.
                                   ]]></xsd:documentation> 
       </xsd:annotation> 
 
       </xsd:attribute> 
 
       <xsd:attribute name="varnames" type="xsd:string" use="optional"> 
 
       <xsd:annotation>

           <xsd:documentation><![CDATA[ 
 
               Define the param name that need to be tested against the set of property source provided in the src attribute.
                  ]]></xsd:documentation> 
       </xsd:annotation>
       </xsd:attribute> 
       <xsd:attribute name="src" type="xsd:string" use="required"> 
 
       <xsd:annotation>
       <xsd:documentation><![CDATA[ 
              Define the property file/xml from which the test param can be loaded.
                  ]]></xsd:documentation> 
       </xsd:annotation>
       </xsd:attribute> 
</xsd:complexType> 
 
</xsd:element>
 
</xsd:schema>






Step 2) Coding a NamespaceHandler

package com.mycompany.product.spring; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; /** * @author <A href="mailto:snc_43@yahoo.com">Surya Choudhury</A> * */ public class ConditionalBeanNamespaceHandler extends NamespaceHandlerSupport { /* (non-Javadoc) * @see org.springframework.beans.factory.xml.NamespaceHandler#init() */ @Override public void init() { super.registerBeanDefinitionParser("condition", new ConditionalBeanDefinitionParser()); } }

Step 3) Coding a BeanDefinitionParser



package com.mycompany.product.spring;
import java.util.Properties;
import org.apache.commons.jexl.Expression;
import org.apache.commons.jexl.ExpressionFactory;
import org.apache.commons.jexl.JexlContext;
import org.apache.commons.jexl.JexlHelper;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.core.io.Resource;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

 
/**
* @author <A href="mailto:snc_43@yahoo.com">Surya Choudhury</A>
*
*/
public class ConditionalBeanDefinitionParser implements BeanDefinitionParser {
       private final Log cLog = LogFactory.getLog(ConditionalBeanDefinitionParser.class);
       private Properties config;


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


      public ConditionalBeanDefinitionParser() {
            config = new Properties();
      }


      /**
      * Parse the "condition" element and check the mandatory "test" attribute. If
      * the provided resources or the system property named by test is null/empty/false
      * (i.e. not defined) then return null, which is the same as not defining the bean.
      */
      public BeanDefinition parse(Element element, ParserContext parserContext) {
             try{
               if (DomUtils.nodeNameEquals(element, "condition")) {
                  String test = element.getAttribute("test");
                  String src = element.getAttribute("src");
                  String varnames = element.getAttribute("varnames"); 
 
                  // Check the src attribute is not empty.
                   if(StringUtils.isNotEmpty(src)){
                      Resource resource = parserContext.getReaderContext().getResourceLoader().getResource(src.trim());
                      config.load(resource.getInputStream());
                   }else{
                      throw new IllegalArgumentException("src attribute not set.");
                   } 
                  // Check if the varnames is not empty/null
                  if(StringUtils.isNotEmpty(test) && StringUtils.isNotEmpty(varnames) && StringUtils.isNotBlank(varnames)){
                     String expression = test.substring(
                                DEFAULT_PLACEHOLDER_PREFIX.length(),
                                test.length() - DEFAULT_PLACEHOLDER_SUFFIX.length()).trim();

                     String[] vars = varnames.split(",");
                     JexlContext jc = JexlHelper.createContext();
                     for(String varname: vars){
                       varname = varname.trim();
                       jc.getVars().put(varname, config.get(varname));
                     }
                     Expression e = ExpressionFactory.createExpression(expression);
                     Object result = e.evaluate(jc);  
                     if( (null != result)){
                       if(result.toString().equalsIgnoreCase("true")){
                          Element beanElement = DomUtils.getChildElementByTagName(element, "bean");
                          return parseAndRegisterBean(beanElement, parserContext);
                       }else if(result.toString().equalsIgnoreCase("false")){
                          return null;
                       }else if ( StringUtils.isNotEmpty(getProperty(test))) {
                          Element beanElement = DomUtils.getChildElementByTagName(element, "bean");
                          return parseAndRegisterBean(beanElement, parserContext);
                       }else{
                          cLog.warn("Condition bean creation did not happen as test or src attribute is not set.");
                       }
                  }
             }
            // Else proceed with non-empty/NULL/Boolean value check
            else{
               if ( StringUtils.isNotEmpty(getProperty(test))) {
                  Element beanElement = DomUtils.getChildElementByTagName(element, "bean");
                  return parseAndRegisterBean(beanElement, parserContext);
                }else{
                  cLog.warn("Condition bean creation did not happen as test or src attribute is not set.");
                }
           }
        }
     }catch (Exception e) {
         cLog.error("Fail to load condition bean.", e);
     }
        return null;
    } 


    /**
     * Get the value of a named resource/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) {
              cLog.info(strVal);
              if (StringUtils.isEmpty(strVal)) {
                  return null;
              } 
              String returnValue = null;
              if (strVal.startsWith(DEFAULT_PLACEHOLDER_PREFIX) && strVal.endsWith(DEFAULT_PLACEHOLDER_SUFFIX)) {
                  returnValue = config.getProperty(
                        strVal.substring(DEFAULT_PLACEHOLDER_PREFIX.length(),
                        strVal.length() - DEFAULT_PLACEHOLDER_SUFFIX.length()).trim());
                  if(null == returnValue){
                      returnValue = System.getProperty(
                              strVal.substring(DEFAULT_PLACEHOLDER_PREFIX.length(),
                              strVal.length() - DEFAULT_PLACEHOLDER_SUFFIX.length()));
                   }
                   if( StringUtils.isNotEmpty(returnValue)){
                      if(returnValue.trim().equalsIgnoreCase("false"))
                          returnValue = null;
                       }
                       if(cLog.isDebugEnabled()){
                           cLog.debug("Returned : "+System.getProperty(
                                     strVal.substring(DEFAULT_PLACEHOLDER_PREFIX.length(),
                                     strVal.length() - DEFAULT_PLACEHOLDER_SUFFIX.length())));
                        }
                        return returnValue;
                     }else{
                        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\://mycompany.com/springbeans/condition=com.mycompany.product.spring.ConditionalBeanNamespaceHandler

META-INF/spring.schemas
http\://mycompany.com/springbeans/condition/condition.xsd=com/mycompany/product/spring/condition.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.

Other conditional operators like: <, <=, >, >=, ==, !=, ||, && etc. are also legal expression

NOTE: As there are few restriction that is been add by the spring-bean-2.0.xsd, so while defining character like <, >, ||, && it is better to use its html/ASCII code, for example a ‘&&’ condition can be noted as ‘&&’ similarly ‘>’ as ‘>’ and ‘<’ as ‘<’ etc.

The attribute varnames is an optional attribute, but when an expression is subjected for evaluation (as in above examples) all the variable names are required to be passed with comma separated as varnames value.

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 or provide a properties file to the “src” attribute [The same parser context loader's ResourceLoader will be used to load the src properties file]

Note: Both test and src are mandatory attributes for <if:condition/>

The spring-beans-2.0.xsd forces you to define a <if:condition/> for single beans – you cannot put a <if:condition/> 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 <if:condition/> conditions guarantee only one of the definitions will be in force at any given time.

No Comments Yet »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a comment

Blog at WordPress.com.