advertisement

Loading environment specific spring configurations.

At my work we try to deploy locally on tomcat or glassfish and in production (and the other test environments) on webspere. For this we often need to overwrite a spring bean to implement it differently. For example we use jdni to loopup the datasource in the websphere environments, but we prefer to use a commons dbcp pool on tomcat. Also workloadmanager, shedulers, and MQ configurations tend to differ on the different locations.

Each application dealt with this in a different manner, so we wanted a general flexible solution to work with different configuration files in different locations. The solution present and the solution we choose where based on the on Joris Kuipers introduced in an older application, he blogged about this in http://blog.springsource.com/2007/06/25/code-samples-from-springone-beyond-the-obvious-talk/

We wanted to put our configuration files in a hierarchical structure as for example the following:

  • context/
  • context/env/{environment} ie context/env/l-tomcat context/env/p etc.

And configure the environment by using a system property.

The idea is that first all configuration files from the base directory and then the environment specific files are loaded. To make this work we extended the ContextLoader from spring the following way:

/**
 * A EnvironmentDependent {@link ContextLoader} that reads the environment property and loads the base + the environment specific context files.
 * The environment specif context is expected under the normal context in a directoy /env/{environment}.
 *
 * If you for example specify a <b>contextConfigLocation</b> param with the following: <b>/WEB-INF/applicationContext.xml,classpath:/context/*Context.xml</b>
 * this loader will load the following if the environment is set to L-TOMCAT:
 * <ul>
 * 	<li>/WEB-INF/applicationContext.xml</li>
 *  <li>/WEB-INF/env/L-TOMCAT/applicationContext.xml</li>
 *  <li>all xml files ending with Context in the classpath directory /context</li>
 *  <li>all xml files ending with Context in the classpath directory /context/env/L-TOMCAT</li>
 *</ul>
 * @author Job de Noo
 */
public class EnvironmentDependentContextLoader extends ContextLoader{
	private static final Logger log = Logger.getLogger(EnvironmentDependentContextLoader.class);

    @Override
    protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) {
	    super.customizeContext(servletContext, applicationContext);
	    String environment = new EnvironmentResolver().getEnvironment();
    	log.info("cusotmizing the context with the EnvironmentDependentContextLoaderListener for environment:" + environment);

	    List<String> configLocations = new ArrayList<String>();
	    configLocations.addAll(Arrays.asList(applicationContext.getConfigLocations()));
    	for (String configLocation : applicationContext.getConfigLocations()){
			if (new AntPathMatcher().isPattern(configLocation)){
	    		handlePattern(configLocations, environment, configLocation);
	    	}else{
	    	    handleNormalLocation(applicationContext, configLocations, environment, configLocation);
	    	}
	    }
	    String[] configLocs = new String[configLocations.size()];
	    configLocations.toArray(configLocs);
	    applicationContext.setConfigLocations(configLocs);
	    log.info("done cusotmizing the context with the EnvironmentDependentContextLoaderListener for environment:" + environment);
	    log.info("loading the following config loactions:" +  configLocs);
    }

	private void handleNormalLocation(ConfigurableWebApplicationContext applicationContext, List<String> configLocations,
            String environment, String configLocation) {
	    String environmentDependentLocation = StringUtils.applyRelativePath(configLocation, "env/" +environment+ "/"+ StringUtils.getFilename(configLocation));
	    Resource overridingResource = applicationContext.getResource(environmentDependentLocation);
	    if (overridingResource.exists()){
	    	configLocations.add(environmentDependentLocation);
	    	log.info("added a environmentspecific location: " + environmentDependentLocation);
	    }
    }

	private void handlePattern(List<String> configLocations, String environment, String configLocation) {
		String environmentDependentLocation;
		if (configLocation.contains("/")){
			environmentDependentLocation = StringUtils.applyRelativePath(configLocation, "env/" +environment+ "/"+ StringUtils.getFilename(configLocation));
		}else{
			int prefixEnd = configLocation.indexOf(":") + 1;
			if (prefixEnd == 0){
				environmentDependentLocation = StringUtils.applyRelativePath(configLocation, "env/" +environment+ "/"+ StringUtils.getFilename(configLocation));
			}else{
		    	String prefix = configLocation.substring(0, prefixEnd);
		    	String pattern = configLocation.substring(prefixEnd);
		    	environmentDependentLocation = prefix + StringUtils.applyRelativePath(pattern, "env/" +environment+ "/"+ StringUtils.getFilename(pattern));
			}
		}
    	log.info("added a environmentspecific location: " + environmentDependentLocation);
		configLocations.add(environmentDependentLocation);
    }
}

Note: In Spring 3 this class should extend the ContextLoaderListener instead of the ContextLoader.

For Spring 2.5 a simple implementation of the ContextLoaderListener that creates the above ContextLoader is enough:

/**
 * ContextLoaderListener implementation that will use the {@link EnvironmentDependentContextLoader} as ContextLoader.
 * @author Job de Noo
 *
 */
public class EnvironmentDependentContextLoaderListener extends ContextLoaderListener{
	private static final Logger log = Logger.getLogger(EnvironmentDependentContextLoader.class);

	@Override
    public ContextLoader createContextLoader() {
		log.info("returning the EnvironmentDependentContextLoader");
		return new EnvironmentDependentContextLoader();
    }
}

To use this contextListener simply replace the ContextLoaderListener configured in the web.xml by this one.
Then if you specify the conf

[/xml]

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:application-context.xml
                        classpath:jms-context.xml
                        WEB-INF/spring-web.xml
              </param-value>
	</context-param>
[xml]

it will load the above files plus the following if they exist:(assuming you environment variable is set to D.
classpath:/env/d/application-context.xml
classpath:/env/d/jms-context.xml
WEB-INF/env/d/spring-web.xml

It also supports patterns like *Context.xml etc.

  • Share/Bookmark

6 comments to Loading environment specific spring configurations.

  • Joris Kuipers

    Such a solution was first introduced in the FIRM application many years ago :)
    I’ve also described some solutions (incl. one that’s very similar to yours) in this blog post:
    http://blog.springsource.com/2007/06/25/code-samples-from-springone-beyond-the-obvious-talk/

    Joris

    PS When I find some time I’ll plan a date with you for those beers you still owe me ;) Might take a while though, as I’m becoming a father again this month…

  • Job

    He Joris,

    yeah i think you started this kind of solution in firm, but is worked differently with a parent context. But the basic idea is indeed from there ;-) And give me a call when you have time to drink those beers, good luck and gz in anvance.

  • Job

    i changed to article to link yours btw.

  • Stevo Slavic

    Since environment resolver is needed, correct me if I’m wrong in assuming that all environment specific contexts are being packaged together in one deployment package regardless of the actual deployment environment. If my assumption is correct, it’s not same but to me this is too similar to bundling test classes with the application in e.g. production deployment unit package, only in this case these are likely different development/test environment specific contexts, but still test stuff, being packaged with production code, which IMO is wrong thing to do.

    If my assumption is wrong, and there is a deployment package for each environment and each deployment package contains only one variation of environment specific context(s), then I don’t see need for environment resolver and extending Spring stuff but I do see some downsides. Why not configure build tool to have in deployment unit package for each environment these environment specific contexts either placed at same classpath location so they can be referenced in same way regardless of the environment, or employ naming convention and use wildcards like having in data layer context? I prefer former (less wildcards less flexibility but more control) so with e.g. Maven 2 as build tool together with help of maven assembly plugin one can at one go build deployment artifacts for multiple different environments, each having only what’s needed for that environment, be it configuration file or environment specific Spring context, all being referenced/loaded in same way regardless of the environment with no need to extend Spring (so less code, less potential bugs and fewer things to worry about during maintenance, like worrying what has to be done when migrating to Spring 3) and no need to configure environment variables or similar stuff for environment resolver (so easier deployment). Adding support for new environment is as flexible as in your/Joris’s solution, one just has to create new environment specific context and extend build tool configuration (in maven2 assembly case by adding new assembly descriptor).

  • Stevo Slavic

    Example with tags was stripped off from “… use wildcards like having in data layer context…”, so I’ll rephrase that part: …use wildcards like having import resource statement reference classpath resource like following: “classpath:com/foo/bar/ctx/env-specific-datasource-config-*.xml” in an (aggregator) data layer context

  • Job

    EnvironmentResolver just lookups the system property that identifies the environment, i’m now on vacance but will add the code to this entry.
    It is true that all environment specific contexts are being packaged. This is because we want to be able to create one war/ear file that passes by the different test environments and goes into production instead of creating a specific build for a certain environment.

    But you could also put the configurations in a shared lib if you like

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>