基於註解的Spring多資料來源配置和使用

無心碼農發表於2015-11-29

前一段時間研究了一下spring多資料來源的配置和使用,為了後期從多個資料來源拉取資料定時進行資料分析和報表統計做準備。由於之前做過的專案都是單資料來源的,沒有遇到這種場景,所以也一直沒有去了解過如何配置多資料來源。
後來發現其實基於spring來配置和使用多資料來源還是比較簡單的,因為spring框架已經預留了這樣的介面可以方便資料來源的切換。
先看一下spring獲取資料來源的原始碼:

可以看到AbstractRoutingDataSource獲取資料來源之前會先呼叫determineCurrentLookupKey方法查詢當前的lookupKey,這個lookupKey就是資料來源標識。
因此透過重寫這個查詢資料來源標識的方法就可以讓spring切換到指定的資料來源了。
第一步:建立一個DynamicDataSource的類,繼承AbstractRoutingDataSource並重寫determineCurrentLookupKey方法,程式碼如下:

1 public class DynamicDataSource extends AbstractRoutingDataSource {
2 
3     @Override
4     protected Object determineCurrentLookupKey() {
5         // 從自定義的位置獲取資料來源標識
6         return DynamicDataSourceHolder.getDataSource();
7     }
8 
9 }

第二步:建立DynamicDataSourceHolder用於持有當前執行緒中使用的資料來源標識,程式碼如下:

 1 public class DynamicDataSourceHolder {
 2     /**
 3      * 注意:資料來源標識儲存線上程變數中,避免多執行緒運算元據源時互相干擾
 4      */
 5     private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<String>();
 6 
 7     public static String getDataSource() {
 8         return THREAD_DATA_SOURCE.get();
 9     }
10 
11     public static void setDataSource(String dataSource) {
12         THREAD_DATA_SOURCE.set(dataSource);
13     }
14 
15     public static void clearDataSource() {
16         THREAD_DATA_SOURCE.remove();
17     }
18 
19 }

第三步:配置多個資料來源和第一步裡建立的DynamicDataSource的bean,簡化的配置如下:

 1 <!--建立資料來源1,連線資料庫db1 -->
 2 <bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 3     <property name="driverClassName" value="${db1.driver}" />
 4     <property name="url" value="${db1.url}" />
 5     <property name="username" value="${db1.username}" />
 6     <property name="password" value="${db1.password}" />
 7 </bean>
 8 <!--建立資料來源2,連線資料庫db2 -->
 9 <bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
10     <property name="driverClassName" value="${db2.driver}" />
11     <property name="url" value="${db2.url}" />
12     <property name="username" value="${db2.username}" />
13     <property name="password" value="${db2.password}" />
14 </bean>
15 <!--建立資料來源3,連線資料庫db3 -->
16 <bean id="dataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
17     <property name="driverClassName" value="${db3.driver}" />
18     <property name="url" value="${db3.url}" />
19     <property name="username" value="${db3.username}" />
20     <property name="password" value="${db3.password}" />
21 </bean>
22 
23 <bean id="dynamicDataSource" class="com.test.context.datasource.DynamicDataSource">  
24     <property name="targetDataSources">  
25         <map key-type="java.lang.String">
26             <!-- 指定lookupKey和與之對應的資料來源 -->
27             <entry key="dataSource1" value-ref="dataSource1"></entry>  
28             <entry key="dataSource2" value-ref="dataSource2"></entry>  
29             <entry key="dataSource3 " value-ref="dataSource3"></entry>  
30         </map>  
31     </property>  
32     <!-- 這裡可以指定預設的資料來源 -->
33     <property name="defaultTargetDataSource" ref="dataSource1" />  
34 </bean>  

到這裡已經可以使用多資料來源了,在運算元據庫之前只要DynamicDataSourceHolder.setDataSource("dataSource2")即可切換到資料來源2並對資料庫db2進行操作了。

示例程式碼如下:

 1 @Service
 2 public class DataServiceImpl implements DataService {
 3     @Autowired
 4     private DataMapper dataMapper;
 5 
 6     @Override
 7     public List<Map<String, Object>> getList1() {
 8         // 沒有指定,則預設使用資料來源1
 9         return dataMapper.getList1();
10     }
11 
12     @Override
13     public List<Map<String, Object>> getList2() {
14         // 指定切換到資料來源2
15         DynamicDataSourceHolder.setDataSource("dataSource2");
16         return dataMapper.getList2();
17     }
18 
19     @Override
20     public List<Map<String, Object>> getList3() {
21         // 指定切換到資料來源3
22         DynamicDataSourceHolder.setDataSource("dataSource3");
23         return dataMapper.getList3();
24     }
25 }

--------------------------------------------------------------------------------------華麗的分割線--------------------------------------------------------------------------------------------------

但是問題來了,如果每次切換資料來源時都呼叫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的執行緒變數中:

 1 public class DataSourceAspect {
 2 
 3     /**
 4      * 攔截目標方法,獲取由@DataSource指定的資料來源標識,設定到執行緒儲存中以便切換資料來源
 5      * 
 6      * @param point
 7      * @throws Exception
 8      */
 9     public void intercept(JoinPoint point) throws Exception {
10         Class<?> target = point.getTarget().getClass();
11         MethodSignature signature = (MethodSignature) point.getSignature();
12         // 預設使用目標型別的註解,如果沒有則使用其實現介面的註解
13         for (Class<?> clazz : target.getInterfaces()) {
14             resolveDataSource(clazz, signature.getMethod());
15         }
16         resolveDataSource(target, signature.getMethod());
17     }
18 
19     /**
20      * 提取目標物件方法註解和型別註解中的資料來源標識
21      * 
22      * @param clazz
23      * @param method
24      */
25     private void resolveDataSource(Class<?> clazz, Method method) {
26         try {
27             Class<?>[] types = method.getParameterTypes();
28             // 預設使用型別註解
29             if (clazz.isAnnotationPresent(DataSource.class)) {
30                 DataSource source = clazz.getAnnotation(DataSource.class);
31                 DynamicDataSourceHolder.setDataSource(source.value());
32             }
33             // 方法註解可以覆蓋型別註解
34             Method m = clazz.getMethod(method.getName(), types);
35             if (m != null && m.isAnnotationPresent(DataSource.class)) {
36                 DataSource source = m.getAnnotation(DataSource.class);
37                 DynamicDataSourceHolder.setDataSource(source.value());
38             }
39         } catch (Exception e) {
40             System.out.println(clazz + ":" + e.getMessage());
41         }
42     }
43 
44 }

最後在spring配置檔案中配置攔截規則就可以了,比如攔截service層或者dao層的所有方法:

1 <bean id="dataSourceAspect" class="com.test.context.datasource.DataSourceAspect" />
2     <aop:config>
3         <aop:aspect ref="dataSourceAspect">
4             <!-- 攔截所有service方法 -->
5             <aop:pointcut id="dataSourcePointcut" expression="execution(* com.test.*.dao.*.*(..))"/>
6             <aop:before pointcut-ref="dataSourcePointcut" method="intercept" />
7         </aop:aspect>
8     </aop:config>
9 </bean>

OK,這樣就可以直接在類或者方法上使用註解@DataSource來指定資料來源,不需要每次都手動設定了。

示例程式碼如下:

 1 @Service
 2 // 預設DataServiceImpl下的所有方法均訪問資料來源1
 3 @DataSource("dataSource1")
 4 public class DataServiceImpl implements DataService {
 5     @Autowired
 6     private DataMapper dataMapper;
 7 
 8     @Override
 9     public List<Map<String, Object>> getList1() {
10         // 不指定,則預設使用資料來源1
11         return dataMapper.getList1();
12     }
13 
14     @Override
15     // 覆蓋類上指定的,使用資料來源2
16     @DataSource("dataSource2")
17     public List<Map<String, Object>> getList2() {
18         return dataMapper.getList2();
19     }
20 
21     @Override
22     // 覆蓋類上指定的,使用資料來源3
23     @DataSource("dataSource3")
24     public List<Map<String, Object>> getList3() {
25         return dataMapper.getList3();
26     }
27 }

提示:註解@DataSource既可以加在方法上,也可以加在介面或者介面的實現類上,優先順序別:方法>實現類>介面。也就是說如果介面、介面實現類以及方法上分別加了@DataSource註解來指定資料來源,則優先以方法上指定的為準。

相關文章