Monday, February 25, 2008

Nested Property Placeholders in Spring Configuration

  1. Using Java Properties in Spring Configuration - the PropertyPlaceholderConfigurer
  2. Using Nested Java Properties in Spring Configuration - nested Place Holders
  3. More Nested Java Properties in Spring Configuration
  4. Caveat for using PropertyPlaceholderConfigurer
  5. Data Source vs JNDI
  6. Appendix

Using Java Properties in Spring Configuration - the PropertyPlaceholderConfigurer

In your Spring configuration (Spring 2.5), it is possible to use property place-holders that get replaced with values from a properties file. For example, consider the Spring configuration below in which I define a POJO (plain old Java object) called testBean and a PropertyPlaceholderConfigurer called placeholderConfig.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  <bean id="placeholderConfig"
      class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="location"
      value="classpath:application.properties" />
  </bean>
  <bean name="testBean" class="my.Test">
    <property name="environmentName">
      <value>${environmentSpecificName}</value>
    </property>
  </bean>
</beans>

Note that the placeholderConfig bean has a location property whose value is the name of a properties file (application.properties) that is expected to appear on the class path. PropertyPlaceholderConfigurer's job is to go through your Spring configuration and replace all property place-holders (a string enclosed by a dollar-curly-brace pair: ${}) with a value from a property file. The property place-holder value is the key to retrieving the value from property file. See below for an important caveat with this behaviour.

Below is the properties file that my Spring configuration references.

# Environment: "local environment", "development environment",
# "test environment", "stress and volume testing environment"
# or "production environment".
environmentSpecificName=local environment

This means that when I retrieve testBean, its environmentName property will be "local environment". Whenever I want to change the value of testBean.environmentName in my code, I change the environmentSpecificName property and re-run my application.

TOP

Using Nested Java Properties in Spring Configuration - nested Place Holders

As you might guess from the comment I placed above the environmentSpecificName property, I want it to vary depending on which environment my code is running within. I.e. I want a different value for each of the environments my code might run within: local, development, testing, SVT and production.

An alternative way I can do this is to set up different properties for each possibility (environment), and use a nested property place-holder in the property value to determine which value should actually be used. My Spring configuration does not change, but my properties file does. Here is my new properties file.

# Environment: local, dev, test, svt or prod.
environmentSpecificName=${local.environmentSpecificName}
local.environmentSpecificName=local environment
dev.environmentSpecificName=development environment
test.environmentSpecificName=test environment
svt.environmentSpecificName=stress and volume testing environment
prod.environmentSpecificName=production environment

Now what happens is that the PropertyPlaceholderConfigurer looks up the value for environmentSpecificName and finds another property place-holder: ${local.environmentSpecificName}. It looks up the value for ${local.environmentSpecificName} and finds local environment.

The advantage here is that I can express each of the possible values as properties and switch between them as needed. It is a bit easier to edit a properties file than a Spring configuration, which tends to get complicated very quickly i.e. arguably, a properties file is more readable than a Spring configuration.

The disadvantage is that I still have to edit a file, which means a re-deploy for a web-app. An Ant build script can help with this, by outputting a different deployable artifact for each environment, using a string replacement (the replace task) to make sure environmentSpecificName is correct.

Why go to all this trouble just to vary one value in the Spring configuration? Forget the place-holders altogether and edit the value directly in the Spring configuration - like you said, an Ant build script can replace the value for us too. This is correct. Everything in this blog entry can be done with an Ant script. The only potential advantage is if you consider it easier to manage options in a properties file than in a build script, or Spring configuration. See the next section for an example of when I prefer to use this approach.

TOP

More Nested Java Properties in Spring Configuration

One situation in which I prefer this approach is when I have to manage multiple values that depend on one condition. Perhaps I have a different database for each environment, or a different set of web service URLs for each environment. I can set up each set of values in a properties file and use a single nested property to switch between sets. This is as close to conditional properties you can get with Spring.

Below is an example Spring configuration in which I define a JdbcTemplate that will come loaded with a data source specific for the environment my code is operating within.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  <bean id="placeholderConfig"
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="ignoreUnresolvablePlaceholders" value="false" />
    <property name="location"
      value="classpath:application.properties" />
  </bean>
  <bean name="jdbcTemplate" singleton="true"
    class="org.springframework.jdbc.core.JdbcTemplate">
    <constructor-arg index="1">
      <ref bean="dataSource" />
    </constructor-arg>
  </bean>
  <bean name="dataSource"
    class="org.springframework.jdbc.datasource.DriverManagerDataSource"
    destroy-method="close">
    <property name="driverClassName">
      <value>${dataSource.driverClassName}</value>
    </property>
    <property name="url">
      <value>${dataSource.url}</value>
    </property>
    <property name="username">
      <value>${dataSource.username}</value>
    </property>
    <property name="password">
      <value>${dataSource.password}</value>
    </property>
  </bean>
</beans>

Here is what my application.properties file might look like.

# Environment: local, dev, test, svt or prod.
environment=local

###############################################################################
## DATABASE PROPERTIES
###############################################################################

dataSource.driverClassName=${${environment}.dataSource.driverClassName}
dataSource.url=${${environment}.dataSource.url}
dataSource.username=${${environment}.dataSource.username}
dataSource.password=${${environment}.dataSource.password}

local.dataSource.url=jdbc:oracle:thin:@oralocal:1521:oralocal
local.dataSource.username=localuser
local.dataSource.password=localpwd
local.dataSource.driverClassName=oracle.jdbc.driver.OracleDriver

dev.dataSource.url=jdbc:oracle:thin:@oradev:1521:oradev
dev.dataSource.username=devuser
dev.dataSource.password=devpwd
dev.dataSource.driverClassName=oracle.jdbc.driver.OracleDriver

test.dataSource.url=jdbc:oracle:thin:@oratest:1521:oratest
test.dataSource.username=testuser
test.dataSource.password=testpwd
test.dataSource.driverClassName=oracle.jdbc.driver.OracleDriver

svt.dataSource.url=jdbc:oracle:thin:@orasvt:1521:orasvt
svt.dataSource.username=svtuser
svt.dataSource.password=svtpwd
svt.dataSource.driverClassName=oracle.jdbc.driver.OracleDriver

prod.dataSource.url=jdbc:oracle:thin:@oraprod:1521:oraprod
prod.dataSource.username=produser
prod.dataSource.password=prodpwd
prod.dataSource.driverClassName=oracle.jdbc.driver.OracleDriver

In this version, I only need to change one property (environment) to have four other properties change in response. Here is what the PropertyPlaceholderConfigurer is doing for ${dataSource.url} and the other place-holders in the data source Spring configured bean.

  1. The property place holder ${dataSource.url} key resolves to the value ${${environment}.dataSource.url}, which is itself a property place holder with another property place holder nested within it.
  2. The property place holder ${environment} key resolves to the value local.
  3. The property place holder ${${environment}.dataSource.url} key is now ${local.dataSource.url}
  4. The property place holder ${local.dataSource.url} resolves to the value jdbc:oracle:thin:@oralocal:1521:oralocal.

TOP

Caveat for using PropertyPlaceholderConfigurer

If you are using an XmlBeanFactory, you have to explicitly reference the PropertyPlaceholderConfigurer and invoke it upon your bean factory. For example.

XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
configurer.setLocation(new FileSystemResource("application.properties"));
configurer.postProcessBeanFactory(factory);

Or you could retrieve the bean from the context and then invoke it.

XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
PropertyPlaceholderConfigurer configurer = factory.getBean("placeholderConfig");
configurer.postProcessBeanFactory(factory);

Either way, you have explicitly invoke the configurer to do its work upon your Spring configuration. For this reason alone, I prefer to use ClassPathXmlApplicationContext as my factory, because it will automatically invoke any PropertyPlaceholderConfigurer on the context, just by having it in the Spring configuration. Also, you generally need a ApplicationContext in web apps.

TOP

Data Source vs JNDI

If you need to manage data sources across environments, and you have access to JNDI in each environment, use that instead. No need to hard code passwords in property files that way. But you will probably still a properties file approach for local unit testing - that you should be able to run without a server.

TOP

Appendix

Links that helped me with this.

  • From the Spring - Java/J2EE Application Framework Reference Documentation is Chapter 3. Beans, BeanFactory and the ApplicationContext.
  • I posted a question about this on the Spring forum, where I learned how to use this functionality. Further, I learned that Spring V3 might allow nested property place-holders in the property keys, not just values.

6 comments:

John said...

Excellent post. It's nice to see that this comes out of the box with 2.5. I had to write custom code to do this in previous versions and often had issues when upgrading to a new version of spring. Well done!

Anonymous said...

It's worth noting that one needs Spring 2.5.3 (or above) for this to work. The release notes for 2.5.3 mention this fix. I spent a couple of hours tracking this down.

springNewbie said...

I like the solution and will use it, but now I need to retrieve the properties from the view. Is there an easy way to do that?

RobertMarkBram said...

Hi SpringNewbie,

The idea is that your Spring configuration puts values into a bean object and you retrieve the bean object from Spring.

See "3.6. Interacting with the BeanFactory" in Chapter 3. Beans, BeanFactory and the ApplicationContext for ways of getting your bean!

Rob
:)

Alexandru Pintilie said...

It's true, this works only since 2.5.3. With 2.5.1 I got the following error "Invalid bean definition with name 'aBean' defined in null: Could not resolve placeholder '${environment'"

Doug said...

Hi Rob,
I was looking for a solution for the nested property and found your post. It's helping me resolve the problem!!!
I would like to add that instead of hard-coding your env variable inside the properties file itself, you can setup a system property -Denv=local for example. Then you don't even have to modify this property file every time you change fromone env to another. Just remember to change the system property though. Like in Tomcat I put -Denv=local in the startup.sh in my CATALINA_BASE/startup.sh file.
Cheers
Doug