AbstractRoutingDataSource 是spring提供的一個多數據源抽象類。spring會在使用事務的地方來調用此類的determineCurrentLookupKey()方法來獲取數據源的key值。咱們繼承此抽象類並實現此方法:java
package com.ctitc.collect.manage.datasource; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * * @author zongbo * 實現spring多路由配置,由spring調用 */ public class DataSourceRouter extends AbstractRoutingDataSource { // 獲取數據源名稱 protected Object determineCurrentLookupKey() { return HandleDataSource.getDataSource(); } }
DataSourceRouter 類中經過HandleDataSource.getDataSource()獲取數據源的key值。此方法應該和線程綁定。spring
package com.ctitc.collect.manage.datasource; /** * 線程相關的數據源處理類 * @author zongbo * */ public class HandleDataSource { // 數據源名稱線程池 private static final ThreadLocal<String> holder = new ThreadLocal<String>(); /** * 設置數據源 * @param datasource 數據源名稱 */ public static void setDataSource(String datasource) { holder.set(datasource); } /** * 獲取數據源 * @return 數據源名稱 */ public static String getDataSource() { return holder.get(); } /** * 清空數據源 */ public static void clearDataSource() { holder.remove(); } }
對於spring來講,註解即簡單方便且可讀性也高。因此,咱們也經過註解在service的方法前指定所用的數據源。咱們先定義本身的註解類,其中value爲數據源的key值。sql
package com.ctitc.collect.manage.datasource; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 數據源註解類 * @author zongbo * */ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { String value(); }
指定註解之後,咱們能夠經過AOP攔截全部service方法,在方法執行以前獲取方法上的註解:即數據源的key值。數據庫
package com.ctitc.collect.manage.datasource; import java.lang.reflect.Method; import java.text.MessageFormat; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** * 切換數據源(不一樣方法調用不一樣數據源) */ @Aspect @Component @Order(1) //請注意:這裏order必定要小於tx:annotation-driven的order,即先執行DataSourceAspect切面,再執行事務切面,才能獲取到最終的數據源 @EnableAspectJAutoProxy(proxyTargetClass = true) public class DataSourceAspect { static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class); /** * 切入點 service包及子孫包下的全部類 */ @Pointcut("execution(* com.ctitc.collect.service..*.*(..))") public void aspect() { } /** * 配置前置通知,使用在方法aspect()上註冊的切入點 */ @Before("aspect()") public void before(JoinPoint point) { Class<?> target = point.getTarget().getClass(); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod() ; DataSource dataSource = null ; //從類初始化 dataSource = this.getDataSource(target, method) ; //從接口初始化 if(dataSource == null){ for (Class<?> clazz : target.getInterfaces()) { dataSource = getDataSource(clazz, method); if(dataSource != null){ break ;//從某個接口中一旦發現註解,再也不循環 } } } if(dataSource != null && !StringUtils.isEmpty(dataSource.value()) ){ HandleDataSource.setDataSource(dataSource.value()); } } @After("aspect()") public void after(JoinPoint point) { //使用完記得清空 HandleDataSource.setDataSource(null); } /** * 獲取方法或類的註解對象DataSource * @param target 類class * @param method 方法 * @return DataSource */ public DataSource getDataSource(Class<?> target, Method method){ try { //1.優先方法註解 Class<?>[] types = method.getParameterTypes(); Method m = target.getMethod(method.getName(), types); if (m != null && m.isAnnotationPresent(DataSource.class)) { return m.getAnnotation(DataSource.class); } //2.其次類註解 if (target.isAnnotationPresent(DataSource.class)) { return target.getAnnotation(DataSource.class); } } catch (Exception e) { e.printStackTrace(); logger.error(MessageFormat.format("經過註解切換數據源時發生異常[class={0},method={1}]:" , target.getName(), method.getName()),e) ; } return null ; } }
假設我有兩個庫:業務庫和訂單庫。先要配置這兩個數據源併發
<bean id="busiDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <description>業務數據源</description> <!-- 數據庫基本信息配置 --> <property name="driverClassName" value="${busi.driverClassName}" /> <property name="url" value="${busi.url}" /> <property name="username" value="${busi.username}" /> <property name="password" value="${busi.password}" /> <!-- 初始化鏈接數量 --> <property name="initialSize" value="${druid.initialSize}" /> <!-- 最大併發鏈接數 --> <property name="maxActive" value="${druid.maxActive}" /> <!-- 最小空閒鏈接數 --> <property name="minIdle" value="${druid.minIdle}" /> <!-- 配置獲取鏈接等待超時的時間 --> <property name="maxWait" value="${druid.maxWait}" /> <!-- 超過期間限制是否回收 --> <property name="removeAbandoned" value="${druid.removeAbandoned}" /> <!-- 超過期間限制多長; --> <property name="removeAbandonedTimeout" value="${druid.removeAbandonedTimeout}" /> <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" /> <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" /> <!-- 用來檢測鏈接是否有效的sql,要求是一個查詢語句--> <property name="validationQuery" value="${druid.validationQuery}" /> <!-- 申請鏈接的時候檢測 --> <property name="testWhileIdle" value="${druid.testWhileIdle}" /> <!-- 申請鏈接時執行validationQuery檢測鏈接是否有效,配置爲true會下降性能 --> <property name="testOnBorrow" value="${druid.testOnBorrow}" /> <!-- 歸還鏈接時執行validationQuery檢測鏈接是否有效,配置爲true會下降性能 --> <property name="testOnReturn" value="${druid.testOnReturn}" /> <!-- 打開PSCache,而且指定每一個鏈接上PSCache的大小 --> <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" /> <property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}" /> <!--屬性類型是字符串,經過別名的方式配置擴展插件,經常使用的插件有: 監控統計用的filter:stat 日誌用的filter:log4j 防護SQL注入的filter:wall --> <property name="filters" value="${druid.filters}" /> </bean>
<bean id="orderDataSource" class="com.alibaba.druid.pool.DruidDataSource" parent="busiDataSource"> <description>訂單數據源</description> <property name="driverClassName" value="${order.driverClassName}" /> <property name="url" value="${order.url}" /> <property name="username" value="${order.username}" /> <property name="password" value="${order.password}" /> </bean>
targetDataSources :數據源列表,key-value形式,即上面配置的兩個數據源
defaultTargetDataSource:默認數據源,若是未指定數據源 或者指定的數據源不存在的話 默認使用這個數據源app
<bean id="dataSource" class="com.ctitc.collect.manage.datasource.DataSourceRouter" lazy-init="true"> <description>多數據源路由</description> <property name="targetDataSources"> <map key-type="java.lang.String" value-type="javax.sql.DataSource"> <!-- write --> <entry key="busi" value-ref="busiDataSource" /> <entry key="order" value-ref="orderDataSource" /> </map> </property> <!-- 默認數據源,若是未指定數據源 或者指定的數據源不存在的話 默認使用這個數據源 --> <property name="defaultTargetDataSource" ref="busiDataSource" /> </bean>
因爲我使用的註解式事務,和咱們的AOP數據源切面有一個順序的關係。數據源切換必須先執行,數據庫事務才能獲取到正確的數據源。因此要明確指定 註解式事務和 咱們AOP數據源切面的前後順序。ide
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" order="2" />
在每一個service方法前使用@DataSource("數據源key")註解便可。性能
@Override @DataSource("busi") @Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public List<BasVersion> test1() { // TODO Auto-generated method stub return coreMapper.getVersion(); } @Override @DataSource("order") @Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public List<BasVersion> test2() { // TODO Auto-generated method stub return coreMapper.getVersion(); }