↓ Skip to main content

Reusable properties exposed as beans in Spring

·719 words·4 mins
Sergii Zolotarov
Author
Sergii Zolotarov
Twenty years of Java, from Kyiv to Leeuwarden. Currently at Booking.com

Wrote a very handy thing which may prove useful to everybody using Spring. It helps treat properties from a property file or system properties as beans of corresponding type (conversion for String, Integer and Boolean is built-in). Bean ids are the same as property names in property file. This is very different from what a default org.springframework.beans.factory.config.PropertyPlaceholderConfigurer, offers, which just fills bean properties with values once.

I also added a simple way to fill in the placeholders in property values themselves. Have a look at unit tests to see how to do this.

Here is the code:


package com.anydoby.spring;

import java.io.IOException;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.StringUtils;

/**
 * PropertyPlaceholderConfigurer for reusable properties. Normally, you cannot
 * retrieve the properties from the PropertyPlaceholderConfigurer but this
 * version has a getProperties() method to do just that.
 */
public class ReusablePropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer implements
  ApplicationContextAware {

 private Properties properties;
 private final static Pattern HOLDER_PATTERN = Pattern.compile("[$][{]([^}]*)[}]");
 private static final Pattern INTEGER_PATTERN = Pattern.compile("\\d{1," + Integer.toString(Integer.MAX_VALUE).length()
   + "}");

 private Properties commandLineArgs = new Properties();
 private BeanDefinitionRegistry registry;

 /**
  * @return the <code>commandLineArgs</code> property
  */
 public final Properties getCommandLineArgs() {
  return this.commandLineArgs;
 }

 /**
  * @param commandLineArgs
  *         the commandLineArgs to set the property to
  */
 public final void setCommandLineArgs(final Properties commandLineArgs) {
  this.commandLineArgs = commandLineArgs;
 }

 /**
  * Delegates the loading to the standard {@link PropertyPlaceholderConfigurer}
  * but also saves the properties so we can reuse them. The saved properties are
  * processed to resolve references to other properties.
  * 
  * @param properties
  *         to load
  */
 @Override
 public void loadProperties(final Properties properties) throws IOException {
  if (this.commandLineArgs != null) {
   properties.putAll(this.commandLineArgs);
  }

  this.properties = new Properties();
  super.loadProperties(properties);

  for (final Entry<Object, Object> entry : properties.entrySet()) {
   if (!this.properties.containsKey(entry.getKey())) {
    this.properties.put(entry.getKey(), entry.getValue());
   }
  }

  for (final Entry<Object, Object> entry : this.properties.entrySet()) {
   final String newValue = resolveValueProperties(this.properties, (String) entry.getValue());
   this.properties.put(entry.getKey(), newValue);
  }

  if (this.registry != null) {
   for (final Entry<Object, Object> entry : this.properties.entrySet()) {
    final Object key = entry.getKey();
    final String beanName = key.toString();
    final Object value = entry.getValue();
    final String stringValue = value.toString();

    final BeanDefinitionBuilder beanDefinitionBuilder;
    if (isBoolean(stringValue)) {
     beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Boolean.class);
    } else if (isInteger(stringValue)) {
     beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Integer.class);
    } else {
     beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(String.class);
    }
    beanDefinitionBuilder.addConstructorArgValue(stringValue);
    beanDefinitionBuilder.setScope(BeanDefinition.SCOPE_SINGLETON);

    final AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
    final BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
    this.logger.debug("Registering " + beanName + " with value " + stringValue + " as "
      + beanDefinition.getBeanClassName());
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
   }
  }
 }

 private boolean isInteger(final String stringValue) {
  return INTEGER_PATTERN.matcher(stringValue).matches();
 }

 private boolean isBoolean(final String stringValue) {
  return Boolean.TRUE.toString().equals(stringValue) || Boolean.FALSE.toString().equals(stringValue);
 }

 /**
  * Returns all properties that have been loaded.
  * 
  * @return loaded properties
  */
 public Properties getProperties() {
  return this.properties;
 }

 /**
  * Resolve the references to properties in the specified value.
  * <p/>
  * Example: value = "${foo} ${bar}" and properties containing a value "Hello"
  * for key "foo" and containing a value "world" for key "bar" will result in
  * "Hello world".
  * 
  * @param properties
  *         the properties to look up values.
  * @param value
  *         the value to resolve. Possibly containing references.
  * @return the resolved value.
  */
 public static String resolveValueProperties(final Properties properties, final String value) {
  final Matcher matcher = HOLDER_PATTERN.matcher(value);
  final StringBuffer sb = new StringBuffer();
  while (matcher.find()) {
   final String holder = matcher.group(0);
   final String holderName = matcher.group(1);
   final String replacement;
   if (!StringUtils.hasText(holderName)) {
    final String property = properties.getProperty(holderName);
    if (property != null) {
     replacement = property;
    } else if (System.getProperty(holderName) != null) {
     replacement = System.getProperty(holderName);
    } else {
     replacement = holder;
    }
   } else {
    replacement = holder;
   }
   matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
  }
  matcher.appendTail(sb);
  return sb.toString();
 }

 @Override
 public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
  findRegistry(applicationContext);
 }

 private void findRegistry(final ApplicationContext applicationContext) {
  if (applicationContext == null) {
   this.logger.warn("Unable to find an application context which provides BeanDefinitionRegistry functionality. "
     + "Properties will not be exposed as beans.");
   return;
  }

  final AutowireCapableBeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory();
  if (beanFactory instanceof BeanDefinitionRegistry) {
   this.registry = (BeanDefinitionRegistry) beanFactory;
  } else {
   findRegistry(applicationContext.getParent());
  }
 }

}

And here you can have a look (not archived) at how to use this in action. Look into the unit test com.anydoby.spring.TestOptionalProperty.


First published on the old anydoby.com blog on 2010-07-20.