Spring AOP根据JdbcTemplate方法名动态设置数据源

Spring AOP根据JdbcTemplate方法名动态设置数据源
http://my.oschina.net/cwalet/blog/36055
说明: 现在的场景是,采用MySQL Replication的方式在两台不同服务器部署并配置主从(Master-Slave)复制;
并需要程序上的数据操作方法访问不同的数据库,比如,update*方法访问主数据库服务器,query*方法访问从数据库服务器,从而减轻读写操作数据库的压力。 即把“增删改”和“查”分开访问两台服务器,当然两台服务器的数据库同步事先已经配置好。
然而程序是早已完成的使用Spring JdbcTemplate的架构,如何在不修改任何源代码的情况下达到此功能呢?
分析:
1.目前有两个数据源需要配置到Spring框架中,如何统一管理这两个数据源?
JdbcTemplate有很多数据库操作方法,关键的可以分为以下几类(使用简明通配符):execute(args..)、update( args.. )、batchUpdate( args.. )、query*( args.. )
2.如何根据这些方法名来使用不同的数据源呢?
3.多数据源的事务管理(暂未处理)
实现:
Spring配置文件applicationContext.xml(包含相关bean类的代码)
1.数据源配置(省略了更为详细的连接参数设置):
view source
print ?
01     <bean id="masterDataSource"
02         class="org.apache.commons.dbcp.BasicDataSource"
03         destroy-method="close">
04         <property name="driverClassName"
05             value="${jdbc.driverClassName}" />
06         <property name="url" value="${jdbc.url}" />
07         <property name="username" value="${jdbc.username}" />
08         <property name="password" value="${jdbc.password}" />
09         <property name="poolPreparedStatements" value="true" />
10         <property name="defaultAutoCommit" value="true" />
11     </bean>
12     <bean id="slaveDataSource"
13         class="org.apache.commons.dbcp.BasicDataSource"
14         destroy-method="close">
15         <property name="driverClassName"
16             value="${jdbc.driverClassName}" />
17         <property name="url" value="${jdbc.url2}" />
18         <property name="username" value="${jdbc.username}" />
19         <property name="password" value="${jdbc.password}" />
20         <property name="poolPreparedStatements" value="true" />
21         <property name="defaultAutoCommit" value="true" />
22     </bean>
23         <bean id="dataSource"
24         class="test.my.serivce.ds.DynamicDataSource">
25         <property name="targetDataSources">
26             <map>
27                 <entry key="master" value-ref="masterDataSource" />
28                 <entry key="slave" value-ref="slaveDataSource" />
29             </map>
30         </property>
31         <property name="defaultTargetDataSource" ref="masterDataSource" />
32     </bean>
首先定义两个数据源(连接地址及用户名等数据存放在properties属性文件中),Spring可以设置多个数据源,究其根本也不过是一个普通bean罢了。
关键是ID为“dataSource”的这个bean的设置,它是这个类“ test.my.serivce.ds.DynamicDataSource ”的一个实例:
view source
print ?
1 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
2   
3 public class DynamicDataSource extends AbstractRoutingDataSource {
4     @Override
5     protected Object determineCurrentLookupKey() {
6         return CustomerContextHolder.getCustomerType();
7     }
8 }
DynamicDataSource 类继承了Spring的抽象类AbstractRoutingDataSource,而 AbstractRoutingDataSource本身实现了javax.sql.DataSource接口(由其父类抽象类AbstractDataSource实现),因此其实际上也是一个标准数据源的实现类。该类是Spring专为多数据源管理而增加的一个接口层,参见Spring-api-doc可知:
Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.
它根据一个数据源唯一标识key来寻找已经配置好的数据源队列,它通常是与当前线程绑定在一起的。
查看其源码,知道它还实现了Spring的初始化方法类InitializingBean,这个类只有一个方法:afterPropertiesSet(),由Spring在初始化bean完成之后调用:
view source
print ?
01 public void afterPropertiesSet() {
02     if (this.targetDataSources == null) {
03         throw new IllegalArgumentException("targetDataSources is required");
04     }
05     this.resolvedDataSources = new HashMap(this.targetDataSources.size());
06     for (Iterator it = this.targetDataSources.entrySet().iterator(); it.hasNext(); ) {
07         Map.Entry entry = (Map.Entry)it.next();
08         Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
09         DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
10         this.resolvedDataSources.put(lookupKey, dataSource);
11     }
12     if (this.defaultTargetDataSource != null)
13         this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
14 }
查看其具体实现可知,Spring将所有已经配置好的数据源存放到一个名为 targetDataSources的hashMap对象中(targetDataSources属性必须设置,否则异常;defaultTargetDataSource属性可以不必设置)。只是把数据源统一存到一个map中并不能做什么,关键是它还重写了javax.sql.DataSourcegetConnection()方法,该方法无论你在何时使用数据库操作相关的方法时都会使用到,即使ibatis、hibernate、JPA等进行多层封装的框架底层还是使用最普通的JDBC来实现。
view source
print ?
01 public Connection getConnection() throws SQLException {
02     return determineTargetDataSource().getConnection();
03 }
04 protected DataSource determineTargetDataSource() {
05     Object lookupKey = determineCurrentLookupKey();
06     DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
07     if (dataSource == null)
08         dataSource = this.resolvedDefaultDataSource;
09     if (dataSource == null)
10         throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
11     return dataSource;
12 }
13 protected Object resolveSpecifiedLookupKey(Object lookupKey) {
14     return lookupKey;
15 }
16 protected abstract Object determineCurrentLookupKey();
省略部分校验代码,这里有一个必须的关键方法:determineCurrentLookupKey,也是一个抽象的有你自己实现的方法,从这个方法返回实际要使用的数据源的key(也即在前面配置的数据源bean的ID)。从 Spring-api-doc中可以看到详细说明:
Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context. Allows for arbitrary keys. The returned key needs to match the stored lookup key type.
它允许任意类型的key,但必须是跟保存到数据源hashMap中的key类型一致。我们可以在Spring配置文件中指定该类型,网上看到有仁兄使用枚举类型的,是一个有不错约束性的主意。
我们的 test.my.serivce.ds.DynamicDataSource ”实现了这个方法,可见具体的数据源key是从CustomerContextHolder类中获得的,并且也是使用key与当前线程绑定的方式:
view source
print ?
01 public class CustomerContextHolder {
02     private static final ThreadLocal contextHolder = new ThreadLocal();
03     public static void setCustomerType(String customerType) {
04         contextHolder.set(customerType);
05     }
06     public static String getCustomerType() {
07         return (String) contextHolder.get();
08     }
09     public static void clearCustomerType() {
10         contextHolder.remove();
11     }
12 }
我们也可以使用全局变量的方式来存储这个key。参见java.lang.ThreadLocal http://t.cn/zWyu2X0
有一位 评论者 一针见血的指出问题来:
Why is userThreadLocal declared public? AFAIK, ThreadLocal instances are typically private static fields. Also, ThreadLocal is a generic type, it is ThreadLocal<T>. An important benefit of ThreadLocal worth mentioning (from 1.4 JVMs forward), is as an alternative to synchronization, to improve scalability in transaction-intensive environments. Classes encapsulated in ThreadLocal are automatically thread-safe in a pretty simple way, since it's clear that anything stored in ThreadLocal is not shared between threads.
ThreadLocal是线程安全的,并且不能在多线程之间共享。根据这个原理,我写了下面的小例子以便进一步理解:
view source
print ?
01 public class Test {
02     private static ThreadLocal tl = new ThreadLocal();
03     public static void main(String[] args) {
04         tl.set("abc");
05         System.out.println(tl.get());
06         new Thread(new Runnable() {
07             public void run() {
08                 System.out.println(tl.get());
09             }
10         }).start();
11     }
12 }
做到这里,我们已经解决了第一个问题,但似乎还没有进入正题,如何根据JdbcTemplate方法名动态设置数据源呢?
2.Spring AOP切入 JdbcTemplate方法配置:
view source
print ?
01     <bean id="ba" class="test.my.serivce.ds.BeforeAdvice" />
02     <aop:config proxy-target-class="true">
03         <aop:aspect ref="ba">
04             <aop:pointcut id="update"
05                 expression="execution(* org.springframework.jdbc.core.JdbcTemplate.update*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.batchUpdate(..))" />
06             <aop:before method="setMasterDataSource"
07                 pointcut-ref="update" />
08         </aop:aspect>
09         <aop:aspect ref="ba">
10             <aop:before method="setSlaveDataSource"
11                 pointcut="execution(* org.springframework.jdbc.core.JdbcTemplate.query*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.execute(..))" />
12         </aop:aspect>
13     </aop:config>
可以看到我已经使用<aop:aspect>将JdbcTemplate的4类方法进行拦截,并使用前置通知的方式( <aop:before> )在执行这些方法之前调用其他方法,具体的AOP表达式语言的含义我就不细说了。
根据最开始的说明,分别对update操作和select操作进行拦截并调用不同的方法,这个方法到底是什么呢?
其实就是给 ThreadLocal 设置数据源的名字(key),以便 DynamicDataSource知道到底是使用哪一个数据源。
前置方法就是调用“ test.my.serivce.ds.BeforeAdvice ”类的某个set方法:
view source
print ?
1 public class BeforeAdvice {
2     public void setMasterDataSource() {
3         CustomerContextHolder.setCustomerType("master");
4     }
5     public void setSlaveDataSource() {
6         CustomerContextHolder.setCustomerType("slave");
7     }
8 }
当前线程就会保存下设置进去的key名称并随时可以调用。
最后再配置一个JdbcTemplate bean即可。
view source
print ?
1 <bean id="jdbcTemplate"
2         class="org.springframework.jdbc.core.JdbcTemplate">
3     <property name="dataSource" ref="dataSource" />
4 </bean>
附注:
1. 在解决过程中遇到的一个问题(参考: http://t.cn/zWyu79h ):
Spring异常:no matching editors or conversion strategy found
引用: Spring注入的是接口,关联的是实现类。 这里注入了实现类,所以报异常了。
2.本文主要参考的文章有:
该文还包含事务管理的配置: http://t.cn/zWyuPsJ
该文与多数据源的设置对我有一定的启发(此外还包含测试用例): http://t.cn/zWyuwV5
之前做过ibatis采用ehCache和osCache做缓存的配置,这篇有点类似: http://t.cn/zWyuwBN
多数据源的一些实际场景分析,理论重于实际: http://t.cn/zWyuPz6
此外,javaeye(现为iteye)的一些文章也是有参考价值的: http://t.cn/zWyuASq
EOF.最初的设想到这里变成了现实。本文讲述了“Spring AOP根据JdbcTemplate方法名动态设置数据源”的整个实现过程和一些浅显的分析。
使用这样配置后在实际使用中发现仍然有问题。比如,调用jdbcTemplate的update方法后立即调用query方法查询该条记录,或者使用以下方法:
this.jdbcTemplate.update(new PreparedStatementCreator(), keyHolder)
因为数据库复制有同步间隙,这个时间晚于程序的调用,就会出现查询不到数据的情况,实际上是数据还未同步到从服务器。期待更好的解决方案!

你可能感兴趣的:(dataSource)