前一段時間研究了一下spring多數據源的配置和使用,爲了後期從多個數據源拉取數據定時進行數據分析和報表統計做準備。由於之前做過的項目都是單數據源的,沒有遇到這種場景,所以也一直沒有去了解過如何配置多數據源。
後來發現其實基於spring來配置和使用多數據源還是比較簡單的,因爲spring框架已經預留了這樣的接口可以方便數據源的切換。
先看一下spring獲取數據源的源碼:
可以看到AbstractRoutingDataSource獲取數據源之前會先調用determineCurrentLookupKey方法查找當前的lookupKey,這個lookupKey就是數據源標識。
因此通過重寫這個查找數據源標識的方法就可以讓spring切換到指定的數據源了。
第一步:創建一個DynamicDataSource的類,繼承AbstractRoutingDataSource並重寫determineCurrentLookupKey方法,代碼如下:
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 從自定義的位置獲取數據源標識 return DynamicDataSourceHolder.getDataSource(); } }
第二步:創建DynamicDataSourceHolder用於持有當前線程中使用的數據源標識,代碼如下:
public class DynamicDataSourceHolder { /** * 注意:數據源標識保存在線程變量中,避免多線程操作數據源時互相干擾 */ private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<String>(); public static String getDataSource() { return THREAD_DATA_SOURCE.get(); } public static void setDataSource(String dataSource) { THREAD_DATA_SOURCE.set(dataSource); } public static void clearDataSource() { THREAD_DATA_SOURCE.remove(); } }
第三步:配置多個數據源和第一步裏創建的DynamicDataSource的bean,簡化的配置如下:
<!--創建數據源1,連接數據庫db1 --> <bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${db1.driver}" /> <property name="url" value="${db1.url}" /> <property name="username" value="${db1.username}" /> <property name="password" value="${db1.password}" /> </bean> <!--創建數據源2,連接數據庫db2 --> <bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${db2.driver}" /> <property name="url" value="${db2.url}" /> <property name="username" value="${db2.username}" /> <property name="password" value="${db2.password}" /> </bean> <!--創建數據源3,連接數據庫db3 --> <bean id="dataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${db3.driver}" /> <property name="url" value="${db3.url}" /> <property name="username" value="${db3.username}" /> <property name="password" value="${db3.password}" /> </bean> <bean id="dynamicDataSource" class="com.test.context.datasource.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <!-- 指定lookupKey和與之對應的數據源 --> <entry key="dataSource1" value-ref="dataSource1"></entry> <entry key="dataSource2" value-ref="dataSource2"></entry> <entry key="dataSource3 " value-ref="dataSource3"></entry> </map> </property> <!-- 這裏可以指定默認的數據源 --> <property name="defaultTargetDataSource" ref="dataSource1" /> </bean>
到這裏已經可以使用多數據源了,在操作數據庫之前只要DynamicDataSourceHolder.setDataSource("dataSource2")即可切換到數據源2並對數據庫db2進行操作了。
示例代碼如下:
@Service public class DataServiceImpl implements DataService { @Autowired private DataMapper dataMapper; @Override public List<Map<String, Object>> getList1() { // 沒有指定,則默認使用數據源1 return dataMapper.getList1(); } @Override public List<Map<String, Object>> getList2() { // 指定切換到數據源2 DynamicDataSourceHolder.setDataSource("dataSource2"); return dataMapper.getList2(); } @Override public List<Map<String, Object>> getList3() { // 指定切換到數據源3 DynamicDataSourceHolder.setDataSource("dataSource3"); return dataMapper.getList3(); } }
--------------------------------------------------------------------------------------華麗的分割線--------------------------------------------------------------------------------------------------
但是問題來了,如果每次切換數據源時都調用DynamicDataSourceHolder.setDataSource("xxx")就顯得十分繁瑣了,而且代碼量大了很容易會遺漏,後期維護起來也比較麻煩。能不能直接通過註解的方式指定需要訪問的數據源呢,比如在dao層使用@DataSource("xxx")就指定訪問數據源xxx?當然可以!前提是,再加一點額外的配置^_^。
首先,我們得定義一個名爲DataSource的註解,代碼如下:
1 @Target({ TYPE, METHOD }) 2 @Retention(RUNTIME) 3 public @interface DataSource { 4 String value(); 5 }
然後,定義AOP切面以便攔截所有帶有註解@DataSource的方法,取出註解的值作爲數據源標識放到DynamicDataSourceHolder的線程變量中:
public class DataSourceAspect { /** * 攔截目標方法,獲取由@DataSource指定的數據源標識,設置到線程存儲中以便切換數據源 * * @param point * @throws Exception */ public void intercept(JoinPoint point) throws Exception { Class<?> target = point.getTarget().getClass(); MethodSignature signature = (MethodSignature) point.getSignature(); // 默認使用目標類型的註解,如果沒有則使用其實現接口的註解 for (Class<?> clazz : target.getInterfaces()) { resolveDataSource(clazz, signature.getMethod()); } resolveDataSource(target, signature.getMethod()); } /** * 提取目標對象方法註解和類型註解中的數據源標識 * * @param clazz * @param method */ private void resolveDataSource(Class<?> clazz, Method method) { try { Class<?>[] types = method.getParameterTypes(); // 默認使用類型註解 if (clazz.isAnnotationPresent(DataSource.class)) { DataSource source = clazz.getAnnotation(DataSource.class); DynamicDataSourceHolder.setDataSource(source.value()); } // 方法註解可以覆蓋類型註解 Method m = clazz.getMethod(method.getName(), types); if (m != null && m.isAnnotationPresent(DataSource.class)) { DataSource source = m.getAnnotation(DataSource.class); DynamicDataSourceHolder.setDataSource(source.value()); } } catch (Exception e) { System.out.println(clazz + ":" + e.getMessage()); } } }
最後在spring配置文件中配置攔截規則就可以了,比如攔截service層或者dao層的所有方法:
<bean id="dataSourceAspect" class="com.test.context.datasource.DataSourceAspect" /> <aop:config> <aop:aspect ref="dataSourceAspect"> <!-- 攔截所有service方法 --> <aop:pointcut id="dataSourcePointcut" expression="execution(* com.test.*.dao.*.*(..))"/> <aop:before pointcut-ref="dataSourcePointcut" method="intercept" /> </aop:aspect> </aop:config> </bean>
OK,這樣就可以直接在類或者方法上使用註解@DataSource來指定數據源,不需要每次都手動設置了。
示例代碼如下:
@Service // 默認DataServiceImpl下的所有方法均訪問數據源1 @DataSource("dataSource1") public class DataServiceImpl implements DataService { @Autowired private DataMapper dataMapper; @Override public List<Map<String, Object>> getList1() { // 不指定,則默認使用數據源1 return dataMapper.getList1(); } @Override // 覆蓋類上指定的,使用數據源2 @DataSource("dataSource2") public List<Map<String, Object>> getList2() { return dataMapper.getList2(); } @Override // 覆蓋類上指定的,使用數據源3 @DataSource("dataSource3") public List<Map<String, Object>> getList3() { return dataMapper.getList3(); } }
提示:註解@DataSource既可以加在方法上,也可以加在接口或者接口的實現類上,優先級別:方法>實現類>接口。也就是說如果接口、接口實現類以及方法上分別加了@DataSource註解來指定數據源,則優先以方法上指定的爲準。