Spring context bootstrapping with different environment configurations

The Spring Framework contains many useful features, and the configuration settings through property placeholding is one I had been using in almost every project in the recent years. Although you can easily apply it for basic configurations (as in the example below), it is not possible to use it for <import resource="${something}"/> entries, because the imports will be applied before the PropertyPlaceholderConfigurer (or any other BeanFactoryPostProcessor) processes the actual context. In this article I will describe our solution to do that with flexible configuration options.

Update: While the solution described here works for Spring 2.5 and 3.0, the new 3.1 will introduce a unified property management feature that will be the way forward.

The motivation

I've developed a few applications where the different environments required different items, and they were not only the database vendor and jdbc url that was changing, it were fundamental implementation details. For example for a binary file storage:

  • the development environment used the local disk
  • the unit testing environment used only in-memory buffers
  • the production application used Amazon S3 after it wrote the files to the local EBS volume

While you can separate all of these in different files, the actual initialization logic can be pretty hard sometimes, especially in a web application context, because - as it was described in the first paragraph - you are can't use imports conditionally by default.

You can argue that such requirement is non valid and of course there are workarounds (e.g. generating different war.xml for different environments, or using complex factorybean methods), however such solutions have other pros and cons. We have chosen this path.

Loading properties

The example below loads properties from various sources (and ignores the error if some or all is not present). It is published as part of our open-source Calciuum Core utility library.

It will try to load every specified properties file in the classpath and on the file system, merges the loaded properties with the system properties (e.g. java -Dkey=value ...), and finally it will replace every ${key} value in the configuration with its value.

<!-- Content from the com/calciuum/config/properties.xml -->
<bean id="propertyPlaceholderConfigurer"
        class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="ignoreResourceNotFound" value="true" />
    <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />
    <property name="locations">
        <list>
            <value>classpath*:properties/defaults.properties</value>
            <value>classpath*:properties/${props.env.name}.properties</value>

            <value>classpath*:com/calciuum/config/defaults.properties</value>
            <value>classpath*:com/calciuum/config/${props.env.name}.properties</value>

            <value>classpath*:${props.env.classpath}/defaults.properties</value>
            <value>classpath*:${props.env.classpath}/${props.env.name}.properties</value>

            <value>file:${props.env.ext.properties}</value>
        </list>
    </property>
</bean>

<!-- later in the context, or in a different file -->
<bean class="com.calciuum.support.aws.factory.sns.AmazonSNSFactoryBean" p:region="${aws.region}"
        p:accessKeyId="${aws.accessKeyId}" p:secretAccessKey="${aws.secretAccessKey}"/>

It is nice and plain, contains many options that can be applied in a command line setup or as part of the .jar bundle. Feel free to customize it to your needs if required.

Bootstrapping the Spring context

In a standard application you will be able to initialize the ApplicationContext with the required logic or parameters loaded from external files, however in a web application you have no such flexibility in the environment. Nothing is lost though, because Spring contains a simple option to refresh the actual context with even overriding the configuration locations used by the context. The following bootstrapper implementation will do just the same.

package com.calciuum.support.spring.context;
// imports and copyright, check the actual file in the repository

public class SpringContextBootstrapper implements ApplicationContextAware {
    protected String[] configLocations;
    protected AbstractRefreshableConfigApplicationContext context;

    @PostConstruct
    public void init() throws Exception {
        Set<String> configs = new LinkedHashSet<String>();

        // fix locations
        if (configLocations != null)
            for (String s : configLocations)
                configs.add(s);

        // dynamic locations
        for (ConfigLocationProvider p : context.getBeansOfType(ConfigLocationProvider.class).values()) {
            String[] sp = p.getConfigLocations();
            if (sp != null)
                for (String s : sp)
                    configs.add(s);
        }

        context.setConfigLocations(new ArrayList<String>(configs).toArray(new String[configs.size()]));
        context.refresh();
    }

    public void setConfigLocation(String configLocation) {
        String[] parts = configLocations.split("\\,");
        for (int i = 0; i < parts.length; i++)
            parts[i] = parts[i].trim();
        this.configLocations = parts;
    }

    public void setConfigLocations(String[] configLocations) {this.configLocations = configLocations;}
    public void setApplicationContext(ApplicationContext context) throws BeansException {this.context = (AbstractRefreshableConfigApplicationContext) context;}
}

As you have probably noticed, our bootstrap mechanism is able to use fixed and dynamic configuration location too. We achieve the later with the scanning of the initial context for config location providers, which is a simple interface with a simple implementation:

public interface ConfigLocationProvider {
    public String[] getConfigLocations();
}

public class ConfigLocationProviderImpl implements ConfigLocationProvider {
    protected String[] configLocations;
    public String[] getConfigLocations() { return configLocations;}
    public void setConfigLocation(String configLocation) {
        String[] parts = configLocation.split("\\,");
        for (int i = 0; i < parts.length; i++)
            parts[i] = parts[i].trim();
        this.configLocations = parts;
    }
    public void setConfigLocations(String[] configLocations) {this.configLocations = configLocations;}
}

If everything is prepared, the following context configuration will do the magic:

<!-- Content from com/calciuum/config/bootstrap.xml -->
<context:annotation-config />
<import resource="classpath:com/calciuum/config/properties.xml"/>
<bean name="bootstrap" class="com.calciuum.support.spring.context.SpringContextBootstrapper">
    <property name="configLocations">
        <list>
            <!-- repeat this here if you would use the property placeholders again -->
            <value>classpath:com/calciuum/config/properties.xml</value>
        </list>
    </property>
</bean>

<!-- and this is a modified content from one of our live applications -->
<bean class="com.calciuum.support.spring.context.ConfigLocationProviderImpl">
    <property name="configLocations">
        <list>
            <!-- here comes our dynamic config location -->
            <value>classpath:com/example/config/example-${env.descriptor}-context.xml</value>
        </list>
    </property>
</bean>

Conclusions

We have struggled with many alternatives, most of the produced ugly and bloated context initializations by hand or generating different web.xml files. This simple solution is a better kind of magic, allowing us to separate different application parts while combining them on-demand.

While our application is on Spring on 3.0, we will use this solution, but in the future we might migrate to the new unified property management.

Timestamp: 2011-03-02 23:15
blog comments powered by Disqus
Author
István Soós
technology expert, trainer, business consultant and agile coach
More...