项目中需要配置多个数据源,在此记录一下遇到的小问题。
首先在配置文件中配置数据源:
<bean id="dataSource_Default" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 基本属性 url、user、password -->
<property name="url" value="${datasource.url}" />
<property name="username" value="${datasource.username}" />
<property name="password" value="${datasource.password}" />
<property name="driverClassName" value="${datasource.driverClassName}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="1" />
<property name="minIdle" value="1" />
<property name="maxActive" value="20" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="validationQuery" value="SELECT 'x'" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize"
value="40" />
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat" />
</bean>
<bean id="dataSource_Mysql" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 基本属性 url、user、password -->
<property name="url" value="${mysql.datasource.url}" />
<property name="username" value="${mysql.datasource.username}" />
<property name="password" value="${mysq.datasource.password}" />
<property name="driverClassName" value="${mysq.datasource.driverClassName}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="1" />
<property name="minIdle" value="1" />
<property name="maxActive" value="20" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="validationQuery" value="SELECT 'x'" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize"
value="40" />
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat" />
</bean>
配置动态数据管理对象:
<bean id="dataSource" class="com.hnjl.core.db.datasource.DynamicDataSource">
<property name="targetDataSources">
<map>
<entry key="dataSource_Default" value-ref="dataSource_Default" />
<entry key="dataSource_Mysql" value-ref="dataSource_Mysql" />
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSource_Default" />
</bean>
动态数据源管理类DynamicDataSource的内容如下:
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 取得当前使用那个数据源。
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder .getDataSource();
}
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
//重点
super.afterPropertiesSet();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setDbType(String dbType) {
contextHolder.set(dbType);
}
public static String getDbType() {
return ((String) contextHolder.get());
}
public static void clearDbType() {
contextHolder.remove();
}
}
接下来使用Spring AOP实现动态切换:
@Aspect
@Component
public class DataSourceInterceptor {
@Pointcut("execution(* com.junlong.justiceDepartment.service.sftFlyzjgjbxx.SftFlyzjgjbxxService.*(..))")
public void dataSourceMysql(){
}
@Before(value = "dataSourceMysql()")
public void befor() throws Exception {
DbContextHolder.setDataSource("dataSource_Mysql");
System.out.println("befor:"+DbContextHolder.getDataSource());
}
@After(value = "dataSourceMysql()")
public void after(){
DbContextHolder.setDefaultDataSource();
}
}
到此配置就算完成了,但是启动项目你会发现AOP织入不成功,前置通知根本就没有执行。
这是由于Spring与SpringMVC是2个不同的父子容器, @Aspect如果被spring容器加载的话,而@Controller注解的这些类的实例化以及注入却是由SpringMVC来完成。 @Aspect如果被spring容器加载的时候,可能Spring MVC容器还未初始化, Controller类还未初始化,所以无法正常织入。
所以调整如下:
@Aspect
public class DataSourceInterceptor {
去掉@Component注解,然后把 aop:aspectj-autoproxy 移入springmvc配置文件中,并定义bean,如下:
<!--开启动态代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean id="DataSourceInterceptor" class="com.hnjl.core.db.datasource.DataSourceInterceptor"/>
到这一步发现前置通知执行了,实现了DataSouceHolder的切换,只不过数据源没有切换,这是因为事务环境的隔离性。
Spring中的事务是通过AOP来实现的,所以当我们自己写AOP拦截的时候,就会碰到事务和自定义AOP执行的先后顺序问题,如本文的动态切换数据源的问题。
这里我们使用Spring Order来定义AOP先后顺序,顺带稍微讲一下这个Spring Order。这里借用Ordered接口里的一句话Higher values are interpreted as lower priority。也就是值越高,优先级越低。反过来就是值越低,优先级越高。
但是这里有个问题,上句话说的越高越低和越低越高指的是在执行方法的前的AOP操作,AOP拦截是按照这个顺序,在执行方法后的AOP操作,反之顺序。
还有就是Spring数据源真正切换的关键是AbstractRoutingDataSource的determineTargetDataSource() 方法,此方法是在建立数据库连接时触发。
而事务是在connection层面管理的,启用事务后,一个事务内部的connection是复用的,所以就算AOP切了数据源字符串,但是数据源并不会被真正修改。
因为事务回滚和本文的@AfterThrowing都是在执行方法后,而且事务回滚顺序要在@AfterThrowing前面完成,否则数据源连接池会被事务锁住。按照上面的分析,这个事务回滚的Order值要大于这个自定义AOP中Order的值。
所以这里DataSourceInterceptor修改如下:
@Aspect
@Order(-100)//保证执行顺序
@Component
public class DataSourceInterceptor {
到这里动态切换数据源就算完成了。