Sunday, November 30, 2008

Environment specific property with Spring

In an application there are lots of properties which vary from one environment to another e.g. database used in development will be different from the one used in production, leading to different connection properties (url, user names and password). Applications using Spring Framework usually fallback on PropertyPlaceholderConfigurer for inserting such values from an external properties file.

I would like to have everything at one place instead of externalizing properties into separate file for each environment. Fortunately Spring Framework provides extension points to create user defined XML elements and classes to instantiate beans using those elements. To have environment specific properties in single configuration file along with other bean definitions, I decided to create a custom element named <property>. Next I will try to explain how it works.

Here is the schema for <property> element, which is used to define different property for each environment-
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://vinodsingh.com/schema/spring"
                   xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                   targetNamespace="http://vinodsingh.com/schema/spring">

    <xsd:element name="property">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string" use="required" />
            <xsd:attribute name="class" type="xsd:string" use="optional" default="java.lang.String" />
            <xsd:attribute name="value" type="xsd:string" use="required" />
            <xsd:attribute name="region" use="optional" default="all">
                <xsd:simpleType>
                    <xsd:restriction base="xsd:string">
                        <xsd:enumeration value="dev" />
                        <xsd:enumeration value="test" />
                        <xsd:enumeration value="integ" />
                        <xsd:enumeration value="stage" />
                        <xsd:enumeration value="uat" />
                        <xsd:enumeration value="prod" />
                        <xsd:enumeration value="all" />
                    </xsd:restriction>
                </xsd:simpleType>
            </xsd:attribute>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>
Now create a bean definition parser to parse the <property> element-
public class PropertyDefinitionParser extends AbstractSingleBeanDefinitionParser {

    /** Region where bean will be available. */
    private String beanRegion = null;

    private static String sysRegion = System.getProperty("region");

    static {
        if (sysRegion == null || sysRegion.length() <= 0)
            sysRegion = "all";
    }

    @Override
    protected Class<?> getBeanClass(Element element) {
        Class<?> clazz = null;
        String className = element.getAttribute("class");
        if (className != null && className.length() > 0) {
            try {
                clazz = Class.forName(className);
            } catch (Exception e) {
                log.error("Failed to load class: " + className, e);
                throw new RuntimeException(e);
            }
        }

        return clazz;
    }

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder bean) {
        beanRegion = element.getAttribute("region");

        bean.addConstructorArgValue(element.getAttribute("value"));
    }

    @Override
    protected void registerBeanDefinition(BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
        if (beanRegion.equals(sysRegion) || "all".equals(beanRegion))
            super.registerBeanDefinition(definition, registry);
    }
}
Based on the above-mentioned schema the resulting bean definitions will looks like-
<property id="jdbc.url" value="jdbc:hsqldb:hsql://dev:9002" region="dev" />
<property id="jdbc.url" value="jdbc:hsqldb:hsql://production:9002" region="prod" />

<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="driverClassName"/>
    <property name="url" ref="jdbc.url"/>
    <property name="username" value="username"/>
    <property name="password" value="password"/>
</bean>
Here jdbc.url property is defined twice with different region value. In bean definition for dataSource the jdbc.url is used as bean reference. The PropertyDefinitionParser will register only one definition of jdbc.url,which belongs to the current runtime region. The region will be passed as a system property to JVM.

2 Comments:

Anonymous said...

this the best article

Ángel said...

Too complicated!!

It is better to overwrite the class PropertyPlaceholderConfigurer to resolve the JVM parameter and take the correspondent property.

Cheers.