Saturday, November 18, 2006

More legacy bean mapping strategies with Spring

My last post covered some basic mapping strategies for accessing legacy beans from within Spring's bean context. This post covers three more strategies to expose legacy beans for which the mapping may not be as evident. In all these cases, wrapper code may need to be written or existing code may need to be slightly modified to expose these beans in Spring.

Exposing legacy configuration

The configuration information in the legacy web application is stored in an external directory in a bunch of properties files. By external directory, I mean that it is not in WEB-INF/classes, where you would normally expect it to be. Even though you may cringe at the thought (I know I did, when I first looked at it), there are a number of benefits. First, changing properties is easier for operations folks to do, since the WAR file does not need to be rebuilt, although the application does need to be bounced for the new configuration to take effect. Second, properties can be reused across other web and standalone applications, resulting in less duplication and creating something of an enterprise level configuration. The downside, of course, is that you need a custom strategy to load and customize properties per environment, rather than use Spring's PropertyPlaceholderConfigurer or Maven's environment based filtering that I wrote about earlier.

Properties in the legacy web application is exposed through a Config bean, which exposes static calls such as this:

1
  String mypropertyValue = Config.getConfig("myproperty").get("key");

This call would go out and load the property file myproperty.properties in the specified external directory (passed in to the application as a system property), if it has not already been loaded in a previous invocation, and get back the value for the property named "key". The properties file itself looks something like this:

1
2
# myproperty.properties
key=value

My objective was to have this value exposed through Spring's PropertyPlaceholderConfigurer in the Spring bean context as ${myproperty.key}. I considered building a custom configurer by extending the PropertyPlaceholderConfigurer, but then I found a JIRA post on Atlassian that discussed strategies to expose configuration specified with Jakarta Commons Configuration, one of which I repurposed for my use.

Basically, what I ended up doing was creating a PropertyExtractor class which iterated through all the properties files in the external directory, and loaded all of these into a single Properties object. The keys for each of these properties was the key itself, prefixed by the basename of the properties file. Once this was done, I could pass in the properties to the PropertiesPlaceholderConfigurer by invoking the getProperties() method on the PropertyExtractor class. The Spring configuration for the PropertyPlaceholderConfigurer is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  <bean id="configPropertiesExtractor" class="com.mycompany.util.ConfigPropertiesExtractor">
    <property name="configDir" value="/path/to/external/config/directory" />
  </bean>

  <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="properties">
      <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="targetObject">
          <ref local="configPropertiesExtractor" />
        </property>
        <property name="targetMethod">
          <value>getProperties</value>
        </property>
      </bean>
    </property>
  </bean>

The code for the PropertyExtractor bean is shown below. It is itself a Spring bean, and is configured using the external directory name. It makes calls to the legacy Config bean to get the properties and rebuild a Properties object which is then injected into the PropertyPlaceholderConfigurer bean. From this point on, all properties can be exposed in the ${property_file_basename.property_key} format within the rest of the context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ConfigPropertiesExtractor {

  private static final Logger LOGGER = Logger.getLogger(ConfigPropertiesExtractor.class);

  public ConfigPropertiesExtractor() {
    super();
  }

  public void setConfigDir(String configDir) {
    Config.setConfigDir(configDir);
  }

  public Properties getProperties() throws Exception {
    Properties props = new Properties();
    File configDir = new File(Config.getConfigDir());
    if ((! configDir.exists()) || (! configDir.isDirectory())) {
      LOGGER.error("Config dir:[" + configDir.getAbsolutePath() + "] does not exist or is not a directory");
      return props;
    }
    File[] cfFiles = configDir.listFiles(new FileFilter() {
      public boolean accept(File pathname) {
        return (pathname.getName().endsWith(".properties"));
      }
    });
    for (File cfFile : cfFiles) {
      String prefix = FilenameUtils.getBaseName(cfFile.getName());
      Properties cfProps = Config.getConfig(prefix).getAll();
      for (Iterator it = cfProps.keySet().iterator(); it.hasNext();) {
        String key = (String) it.next();
        String value = (String) cfProps.getProperty(key);
        props.setProperty(prefix + "." + key, value);
      }
    }
    return props;
  }
}

Exposing a predefined DataSource

The legacy application was based on JDBC, so there was already a class that built and returned a Connection object from a pool. The DBA had spent considerable effort to optimize the connection pool for our environment, so it made sense to use the optimizations. One approach would have been to build our own DriverManagerDataSource using the exact same optimized configurations. The disadvantage of this approach is that the DBA would have to maintain identical information in two different places, or the developers will have to continuously play catch up with every change. A second approach would have been to add an extra method to the class to return a DataSource object instead of a Connection (since Spring's JdbcTemplate requires a DataSource to be built). The second approach is the approach we went with. The extra code to return a DataSource was minimal, since the implementation of getConnection was DataSource.getConnection().

1
2
3
4
5
6
7
public class DbConnectionManager {
  ...
  public static DataSource getDataSource() throws Exception {
    return _dataSource;
  }
  ...
}

The configuration is shorter than the standard one for DriverManagerDataSource, just a call to a static method on a predefined class.

1
2
  <bean id="dataSource" class="com.mycompany.util.db.DbConnectionManager" 
      factory-method="getDataSource" />

Sometimes the legacy database ConnectionManager does not reference a DataSource object. This was the case with another third-party application, which built the Connection using traditional DriverManager calls, relying on the database driver's pooling capabilities. My solution in that case was to build a DataSource wrapper implementation whose getConnection() method delegates to the ConnectionManager's getConnection() method. Obviously, the other required methods need to have sensible defaults as well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class MyDataSource implements DataSource {

  private DbConnectionManager connectionManager;

  public void setConnectionManager(DbConnectionManager connectionManager) {
    this.connectionManager = connectionManager;
  }

  public Connection getConnection() {
    return connectionManager.getConnection();
  }

  // other methods of DataSource
  ...
}

And the configuration for this would go something like this:

1
2
3
  <bean id="dataSource" class="com.mycompany.util.db.MyDataSource">
    <property name="connectionManager" ref="dbConnectionManager" />
  </bean>

Accessing objects from a factory

This arose out of a desire to remove some boiler-plate code out of my own pre-Spring code. The code parsed XML using the DOM parser. The pattern for parsing an XML file is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...
  public void doSomethingWithXmlFile() {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    // set properties for factory
    dbf.setValidating(false);
    dbf.setIgnoringElementContentWhitespace(true);
    DocumentBuilder builder = dbf.newDocumentBuilder();
    // set properties for the builder
    builder.setEntityResolver(myEntityResolver);
    // finally parse the XML to get our Document object
    Document doc = builder.parse(xmlFile);
    ...
  }
...

I wanted to just pass a pre-built DocumentBuilder object to the class, and be done with the boilerplate code on top. I achieved this with the following configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  <bean id="documentBuilderFactory" class="javax.xml.parsers.DocumentBuilderFactory" factory-method="newInstance">
    <property name="validating" value="false" />
    <property name="ignoringElementContentWhitespace" value="true" />
  </bean>

  <bean id="documentBuilder" class="javax.xml.parsers.DocumentBuilder"
      factory-bean="documentBuilderFactory" factory-method="newDocumentBuilder">
    <property name="entityResolver" ref="myEntityResolver" />
  </bean>
  ...
  <!-- documentBuilder can now be referenced in a bean definition -->

and the resulting code after moving the boilerplate out to Spring looked like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
  // the setter for Spring
  public void setDocumentBuilder(DocumentBuilder documentBuilder) {
    this.documentBuilder = documentBuilder;
  }

  public void doSomethingWithXmlFile() {
    Document doc = documentBuilder.parse(xmlFile);
    ...
  }
...

All the three mapping strategies described are quite complex, and are not readily apparent. However, the XML metalanguage provided by Spring to configure beans is quite powerful and has lots of features. The power of the metalanguage becomes most evident when one has to expose legacy beans rather than ones which are already exposable using Spring's standard setter injection. As I dig deeper into the legacy code and have to interface with more legacy beans, I am sure I will come across more complex situations, solutions to which I will probably share if I think they are useful.

Be the first to comment. Comments are moderated to prevent spam.