Saturday, November 03, 2007

Dynamic Datasource via Spring using HotSwappableTargetSource

My goal was to create a jar file that encapsulates an application domain objects and their persistence. However, I wanted to provide a way for the calling routines to change the datasource as needed. This feature was not intended to allow swapping datasources in the middle of an application's running but rather to allow the jar file to be ignorant of the test and production configurations. We'll start by creating hypersonic.properties:
jdbc.driver=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:.
jdbc.username=sa
jdbc.password=

hibernate.dialect=org.hibernate.dialect.HSQLDialect
hibernate.hbm2ddl.auto=update
Then create hypersonicContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<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.0.xsd">
 
 <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="location" value="hypersonic.properties"/>
 </bean>
 
 <import resource="beanDefinition.xml"/> 
 
</beans>
I had developed this technique of having a separate set of property file and Spring configuration file for each database that I wanted to access. I've included it in this entry as a side note. The beanDefinition.xml file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<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.0.xsd">
 
 <!--
 This is the bean that the application 'sees' as the datasource. Underneath the covers, the real
 datasource is swapped as needed.
 -->
 <bean id="dataSource" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="targetSource" ref="swappableDataSource"/>
 </bean>
 
 <!--
 This is a magic bean from Spring that allows the underlying (or real) datasource to be
 swapped.
 -->
 <bean name="swappableDataSource" class="org.springframework.aop.target.HotSwappableTargetSource">
  <constructor-arg ref="dummyDataSource"/>
 </bean>
 
 <!-- 
 This dummy datasource is here just to show that you can start off with zero information about the 
 datasource. Later, as the program is running and the datasource information becomes known you 
 can hot swap to the right datasource. 
 -->
 <bean id="dummyDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"/>
 
 <!--
 This datasource shows how to use properties set by an instance of PropertyPlaceholderConfigurer.
 You can define as many of these types of datasources as you'd like. Switch between them 
 -->
 <bean id="defaultDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  <property name="driverClassName" value="${jdbc.driver}" />
  <property name="url" value="${jdbc.url}" />
  <property name="username" value="${jdbc.username}" />
  <property name="password" value="${jdbc.password}" />
 </bean>
 
</beans>
The comments in the XML should explain what is happening. Now comes the DataSourceFactory class which contains the magic.
package factory;

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;

import com.judoscript.JudoEngine;

/**
 * This class allows programs to dynamically change the dataSource 
 * they use. For example, Spring can be configured to know about
 * three beans - defaultDatasource, testDataSource, and 
 * prodDataSource. During development,the defaultDataSource is used 
 * because the getDataSource() method is called without any other 
 * specifications. When the code is deployed to the test environment, that
 * process creates a system property called 'datasource.spring.beanname' 
 * set to testDataSource. Spring's HotSwappableTargetSource
 * feature is used to dynamically switch to the testDataSource bean 
 * previously defined in the Spring configuration files.
 * 
 * 1. Read system property (datasource.script) to get judoscript to execute.
 * 
 * 2. Read system property (datasource.script.filename) to get name 
 * of judoscript file to execute.
 * 
 * 3. Read system property (datasource.spring.beanname) to get name of 
 * Spring bean to load.
 */
public class DataSourceFactory {

 private Log log = LogFactory.getLog(getClass());

 private ApplicationContext ctx = null;

 public DataSourceFactory(ApplicationContext _ctx) {
  super();
  this.ctx = _ctx;
 }

 public DataSource getDataSource() {
  DataSource realDataSource = null;

  String dataSourceScript = System.getProperty("datasource.script");
  String judoScriptFileName = System.getProperty("datasource.script.filename");
  String springBeanName = System.getProperty("datasource.spring.beanname");
  if (dataSourceScript != null) {
   realDataSource = helperFromJudoScriptString(dataSourceScript);
   log.debug("defining DataSource from JudoScript string, via system property.");
  } else if (judoScriptFileName != null) {
   realDataSource = helperFromJudoScriptFile(judoScriptFileName);
   log.debug("defining DataSource from JudoScript script, via system property, named [" + judoScriptFileName + "].");
  } else if (springBeanName != null) {
   realDataSource = (DataSource) ctx.getBean(springBeanName);
   log.debug("defining DataSource from Spring bean, via system property, named [" + springBeanName + "].");
  } else {
   realDataSource = (DataSource) ctx.getBean("defaultDataSource");
   log.debug("defining DataSource from default Spring bean [defaultDataSource] in Spring configuration.");
  }
  return swapToDataSource(realDataSource);
 }

 public DataSource getDataSourceFromSpringBean(final String name) {
  DataSource realDataSource = (DataSource) ctx.getBean(name);
  log.debug("defining DataSource from Spring bean named [" + name + "].");
  return swapToDataSource(realDataSource);
 }
 
 public DataSource getDataSourceFromJudoScriptString(final String script) {
  DataSource realDataSource = helperFromJudoScriptString(script);
  log.debug("defining DataSource from JudoScript string.");
  return swapToDataSource(realDataSource);
 }
 
 public DataSource getDataSourceFromJudoScriptFile(final String filename) {
  DataSource realDataSource = helperFromJudoScriptFile(filename);
  log.debug("defining DataSource from JudoScript file called [" + filename + "].");
  return swapToDataSource(realDataSource);
 }
 
 public DataSource getDataSourceFromDbcpBasicDataSource(final String driverClassName, final String url, final String username, final String password) {
  BasicDataSource realDataSource = new BasicDataSource();
  realDataSource.setDriverClassName(driverClassName);
  realDataSource.setUrl(url);
  realDataSource.setUsername(username);
  realDataSource.setPassword(password);
  return swapToDataSource(realDataSource);
 }
 
 private DataSource swapToDataSource(final DataSource realDataSource) {
  Assert.notNull(realDataSource, "Error defining the real dataSource.");
  HotSwappableTargetSource swapper = (HotSwappableTargetSource) ctx.getBean("swappableDataSource");
  swapper.swap(realDataSource);
  return (DataSource) ctx.getBean("dataSource");
 }

 private DataSource helperFromJudoScriptFile(final String filename) {
  Map sysprops = new HashMap();
  String[] jeArgs = {};
  JudoEngine je = null;
  DataSource rv = null;

  // define the datasource via judoscript.
  try {
   je = new JudoEngine();
   je.putBean("root", new HashMap());
   je.runScript(filename, jeArgs, sysprops);
   Map root = (Map) je.getBean("root");
   rv = (DataSource) root.get("dataSource");
  } catch (Throwable e) {
   throw new RuntimeException(e);
  }

  return rv;
 }

 private DataSource helperFromJudoScriptString(final String script) {
  Map sysprops = new HashMap();
  String[] jeArgs = {};
  JudoEngine je = null;
  DataSource rv = null;

  // define the datasource via judoscript.
  try {
   je = new JudoEngine();
   je.putBean("root", new HashMap());
   je.runCode(script, jeArgs, sysprops);
   Map root = (Map) je.getBean("root");
   rv = (DataSource) root.get("dataSource");
  } catch (Throwable e) {
   throw new RuntimeException(e);
  }

  return rv;
 }

}
Finally, all of the pieces come together in an example program.
package drivers;

import java.sql.Connection;

import javax.sql.DataSource;

import factory.DataSourceFactory;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DatabasePopulationServiceDriver {

 public static void main(String[] args) {
  
  ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"hypersonicContext.xml"});

  try {
   DataSourceFactory dataSourceFactory = new DataSourceFactory(ctx);

                        // If the getDataSource() method is used then
                        // start the program with -D to define the system
                        // property which controls the data source.
   DataSource dataSource = dataSourceFactory.getDataSource();

                        // Or one of the more specific method can be used.
                        DataSource dataSource = dataSourceFactory.getDataSourceFromSpringBean("prodDataSource");

   Connection connection = dataSource.getConnection();
   connection.close();

  } catch (Exception e) {
   e.printStackTrace();
  } finally {
   System.out.println("Done.");
  }
 }

}
Post a Comment