Sunday, November 8, 2009

Web Application Configuration - custom settings per target environment

The issue comes up in every new project - how to create a custom build or customized configuration for each environment. There will be different configurations for JDBC connections, SOAP endpoints, etc. loaded as ResourceBundles, Spring properties, and so on.

Stack overflow has a discussion on configuration patterns where most of the potential solutions are represented. Many externalize the files (not packaged in the war/ear) to a hardcoded file path or to a location on the appserver classpath. Other solutions have multiple properties files and rely on the build script to replace the main application.properties.

These are good solutions, elements of each have been used in projects I've worked on. The externalized properties file or jar is often more hassle in reality, as the build guys are often not the developers - and every extra deploy step is more application downtime.

The build script option is also something I wish to avoid. I want my maven pom to speak for itself with no extra plugins or build hassles.

In my experience, the best solution is multiple properties files, one per environment, bundled in the ear/war and a single set-and-forget JVM property.

The key is letting the application choose a config based on where it's deployed. It's a bad to use IP or hostname for such choices, so choose something that's done once, but is still within the developers/build guys control.

When you setup each application server: set a JVM custom property called "ENV_NAME" that contains the environment name indicator. e.g. local, dev, stage, prod.

This value can then be used anywhere in your J2EE application, from front-end to back as simply as:
    if ("dev".equals(System.getProperty("ENV_NAME")) {
// do what you need for dev env.
}


Make testing easier by loading custom props! Add a static block to your base TestCase class, and call:
System.setProperty("ENV_NAME", "local")


Pretty clunky so far, but now we can take advantage of the Spring property configurator to load environment specific files. (This version was tested on spring 2.5, but have been using this setup since 1.2).

Create a default myapp-config.properties in the classpath root. This is the fallback.

Add a folder for each target environment to your src classpath.

Copy myapp-config.properties with customized values for the target environment to a new folder matching ENV_NAME; e.g. /dev/myapp-config.properties containing the dev JDBC settings.

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

<!-- Configurer that replaces ${..} placeholders with values from properties
Customized to load properties file from classpath, based on path from JVM ENV_NAME value
e.g. ENV_NAME=dev Configurer will look for classpath:/dev/myapp-registration.properties -->
<bean id="propertyConfigurer" class="com...myapp.util.EnvNameBasedConfigurer">
<property name="ignoreResourceNotFound" value="true" />
<property name="locations">
<list>
<value>myapp-config.properties</value>
</list>
</property>
</bean>


I'm a little behind the times already, because I think spring3 can already handle this sort of thing - but not every app is ready to go with spring3 yet.

Added bonus example called in the constructor; loading a customized log4j setup file.


/**
* This class extends the Spring Property Configurer for the
* purpose of loading environment specific property files. This
* implementation relies on a JVM system propery named
* "ENV_NAME" the value sets up which classpath
* folder name property files will be loaded.
*
* For example if ENV_NAME == dev, the resource[s] will be
* loaded from "classpath:/dev/resourcename"
*
* The WebSphere administrator should set ENV_NAME as a custom JVM
* property in each server instance Process Definition.
* JBoss users should alter JAVA_OPTS setting in the run.bat files.
* Standard (not enforced) should be: local, dev, uat, or prod.
*
* @author s.n.beynon
* @version $Revision: 1.4 $ $Date: 2009/11/03 16:46:52 $
*/
public class EnvNameBasedConfigurer extends PropertyPlaceholderConfigurer {

private static Logger log = null;
public static final String SEP = "/";
public static final String APPNAME = "myapp";
private static final String LOGSUFFIX = "-log4j.xml";

public EnvNameBasedConfigurer() {
super();
setup();
}
/**
* @return String env name; dev, uat, prod - default return blank
*/
public static String env() {
return System.getProperty("ENV_NAME", "");
}

/*
* normally would do this setup stuff in an initializing servlet
* call here because we want our custom log config setup before
* spring loads our configuration.
*/
private void setup() {
System.out.println("spring and log4j configuration loading.");
try {
// standard environment log setup (unix log path)
String logconfig = APPNAME + LOGSUFFIX;
// configure different logging based on ENV here.
// developers all use win32; appname-win-log4j.xml
// dev/uat/prod are unix; appname-envname-log4j.xml
if (System.getProperty("os.name").indexOf("Win") > -1) {
logconfig = APPNAME + "-win" + LOGSUFFIX;
}
else if (!"".equals(env())) {
//
logconfig = APPNAME + "-" + env() + LOGSUFFIX;
}
// from classpath load the standard log4j.xml
URL url = Loader.getResource(logconfig);
// custom environment log config not found, fallback to default
if (url == null) logconfig = APPNAME + LOGSUFFIX;
System.out.println("Configuring log from config file: " + url.getFile());
DOMConfigurator.configure(url);

log = Logger.getLogger(EnvNameBasedConfigurer.class);
} catch (Throwable t) {
t.printStackTrace();
}
}

/**
* Override the Property Loader resource configuration based on System
* Property value "ENV_NAME". If ENV_NAME is set, property file will be
* loaded from a directory named by that value (in the classpath).

*
For example, System.setProperty("local") will load files
* from the "local" directory.

If that directory does not
* contain the requested class path resource, will fall back to original
* name.
*
* @see org.springframework.core.io.support.PropertiesLoaderSupport#setLocation(org.springframework.core.io.Resource)
*/
public void setLocation(Resource resource) {

Resource localized = new ClassPathResource(env() + SEP + resource.getFilename());
if ("".equals(env())) {
log.warn("Expected to find system property ENV_NAME - Not Found!");
localized = new ClassPathResource(resource.getFilename());
}
if (localized.exists()) {
super.setLocation(localized);
return;
}
// fall back to what was supplied to this method
super.setLocation(resource);
}

/**
* Override the Property Loader resource configuration based on System
* Property value "ENV_NAME". If ENV_NAME is set, property files will be
* loaded from a directory named by that value (in the classpath).

*
For example, System.setProperty("local") will load files
* from the "local" directory.

If that directory does not
* contain the requested class path resource, will fall back to original
* name.
*
* @see org.springframework.core.io.support.PropertiesLoaderSupport#setLocations(org.springframework.core.io.Resource[])
*/
public void setLocations(Resource[] resources) {
log.debug("EnvNameBasedConfigurer setLocations from classpath for ENV_NAME:" + env());
if ("".equals(env())) {
log.warn("Expected to find system property ENV_NAME - Not Found!");
}

Resource[] envResources = new Resource[resources.length];
for (int i = 0; i < resources.length; i++) {
log.debug("setLocations " + resources[i]);
try {
Resource localized = new ClassPathResource(env() + SEP
+ resources[i].getFilename());
if (localized.exists()) {
envResources[i] = localized;
}
else {
envResources[i] = new ClassPathResource(resources[i].getFilename());
}
log.debug(envResources[i]);
} catch (Exception e) {
log.warn(e);
}
}
super.setLocations(envResources);
}

}


This one class demonstrates using ENV_NAME - a static System Property set on the JVM startup, thus available at any time - to load a custom log4j configuration for the environment. The same method can be used to load a custom ResourceBundle or static properties.

It is important to have a default/fallback configuration, this should be the production configuration. If the ENV_NAME is missing, then it's likely the production resources are firewalled from accidental dev/uat access, and if the ENV_NAME is missing in production then the default configuration is the correct file.

0 comments: