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

说明:如今的场景是,采用MySQL Replication的方式在两台不一样服务器部署并配置主从(Master-Slave)复制;java

并须要程序上的数据操做方法访问不一样的数据库,好比,update*方法访问主数据库服务器,query*方法访问从数据库服务器,从而减轻读写操做数据库的压力。即把“增删改”和“查”分开访问两台服务器,固然两台服务器的数据库同步事先已经配置好。spring

然而程序是早已完成的使用Spring JdbcTemplate的架构,如何在不修改任何源代码的状况下达到此功能呢?sql

分析:数据库

1.目前有两个数据源须要配置到Spring框架中,如何统一管理这两个数据源?express

JdbcTemplate有不少数据库操做方法,关键的能够分为如下几类(使用简明通配符):execute(args..)、update(args..)、batchUpdate(args..)、query*(args..)apache

2.如何根据这些方法名来使用不一样的数据源呢?api

3.多数据源的事务管理(暂未处理)缓存

实现:安全

Spring配置文件applicationContext.xml(包含相关bean类的代码)服务器

1.数据源配置(省略了更为详细的链接参数设置):

    <bean id="masterDataSource"
		class="org.apache.commons.dbcp.BasicDataSource"
		destroy-method="close">
		<property name="driverClassName"
			value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="poolPreparedStatements" value="true" />
		<property name="defaultAutoCommit" value="true" />
	</bean>
	<bean id="slaveDataSource"
		class="org.apache.commons.dbcp.BasicDataSource"
		destroy-method="close">
		<property name="driverClassName"
			value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url2}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="poolPreparedStatements" value="true" />
		<property name="defaultAutoCommit" value="true" />
	</bean>
        <bean id="dataSource"
		class="test.my.serivce.ds.DynamicDataSource">
		<property name="targetDataSources">
			<map>
				<entry key="master" value-ref="masterDataSource" />
				<entry key="slave" value-ref="slaveDataSource" />
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="masterDataSource" />
	</bean>

首先定义两个数据源(链接地址及用户名等数据存放在properties属性文件中),Spring能够设置多个数据源,究其根本也不过是一个普通bean罢了。

关键是ID为“dataSource”的这个bean的设置,它是这个类“test.my.serivce.ds.DynamicDataSource”的一个实例:

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
	@Override
	protected Object determineCurrentLookupKey() {
		return CustomerContextHolder.getCustomerType();
	}
}

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完成以后调用:

public void afterPropertiesSet() {
	if (this.targetDataSources == null) {
		throw new IllegalArgumentException("targetDataSources is required");
	}
	this.resolvedDataSources = new HashMap(this.targetDataSources.size());
	for (Iterator it = this.targetDataSources.entrySet().iterator(); it.hasNext(); ) {
		Map.Entry entry = (Map.Entry)it.next();
		Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
		DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
		this.resolvedDataSources.put(lookupKey, dataSource);
	}
	if (this.defaultTargetDataSource != null)
		this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}

查看其具体实现可知,Spring将全部已经配置好的数据源存放到一个名为targetDataSources的hashMap对象中(targetDataSources属性必须设置,不然异常;defaultTargetDataSource属性能够没必要设置)。只是把数据源统一存到一个map中并不能作什么,关键是它还重写了javax.sql.DataSourcegetConnection()方法,该方法不管你在什么时候使用数据库操做相关的方法时都会使用到,即便ibatis、hibernate、JPA等进行多层封装的框架底层仍是使用最普通的JDBC来实现。

public Connection getConnection() throws SQLException {
	return determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
	Object lookupKey = determineCurrentLookupKey();
	DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
	if (dataSource == null)
		dataSource = this.resolvedDefaultDataSource;
	if (dataSource == null)
		throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
	return dataSource;
}
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
	return lookupKey;
}
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与当前线程绑定的方式:

public class CustomerContextHolder {
	private static final ThreadLocal contextHolder = new ThreadLocal();
	public static void setCustomerType(String customerType) {
		contextHolder.set(customerType);
	}
	public static String getCustomerType() {
		return (String) contextHolder.get();
	}
	public static void clearCustomerType() {
		contextHolder.remove();
	}
}

咱们也可使用全局变量的方式来存储这个key。参见java.lang.ThreadLocalhttp://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是线程安全的,而且不能在多线程之间共享。根据这个原理,我写了下面的小例子以便进一步理解:

public class Test {
	private static ThreadLocal tl = new ThreadLocal();
	public static void main(String[] args) {
		tl.set("abc");
		System.out.println(tl.get());
		new Thread(new Runnable() {
			public void run() {
				System.out.println(tl.get());
			}
		}).start();
	}
}

作到这里,咱们已经解决了第一个问题,但彷佛尚未进入正题,如何根据JdbcTemplate方法名动态设置数据源呢?

2.Spring AOP切入JdbcTemplate方法配置:

    <bean id="ba" class="test.my.serivce.ds.BeforeAdvice" />
	<aop:config proxy-target-class="true">
		<aop:aspect ref="ba">
			<aop:pointcut id="update"
				expression="execution(* org.springframework.jdbc.core.JdbcTemplate.update*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.batchUpdate(..))" />
			<aop:before method="setMasterDataSource"
				pointcut-ref="update" />
		</aop:aspect>
		<aop:aspect ref="ba">
			<aop:before method="setSlaveDataSource"
				pointcut="execution(* org.springframework.jdbc.core.JdbcTemplate.query*(..)) || execution(* org.springframework.jdbc.core.JdbcTemplate.execute(..))" />
		</aop:aspect>
	</aop:config>

能够看到我已经使用<aop:aspect>将JdbcTemplate的4类方法进行拦截,并使用前置通知的方式(<aop:before>)在执行这些方法以前调用其余方法,具体的AOP表达式语言的含义我就不细说了。

根据最开始的说明,分别对update操做和select操做进行拦截并调用不一样的方法,这个方法究竟是什么呢?

其实就是给ThreadLocal设置数据源的名字(key),以便DynamicDataSource知道究竟是使用哪个数据源。

前置方法就是调用“test.my.serivce.ds.BeforeAdvice”类的某个set方法:

public class BeforeAdvice {
	public void setMasterDataSource() {
		CustomerContextHolder.setCustomerType("master");
	}
	public void setSlaveDataSource() {
		CustomerContextHolder.setCustomerType("slave");
	}
}

当前线程就会保存下设置进去的key名称并随时能够调用。

最后再配置一个JdbcTemplate bean便可。

<bean id="jdbcTemplate"
		class="org.springframework.jdbc.core.JdbcTemplate">
	<property name="dataSource" ref="dataSource" />
</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)

由于数据库复制有同步间隙,这个时间晚于程序的调用,就会出现查询不到数据的状况,其实是数据还未同步到从服务器。期待更好的解决方案!

相关文章
相关标签/搜索