1. 程式人生 > >基於spring的aop實現多數據源動態切換

基於spring的aop實現多數據源動態切換

get 聲明式事務 數據庫 abstract 多數據源動態切換 for web開發 pro model

https://lanjingling.github.io/2016/02/15/spring-aop-dynamicdatasource/

基於spring的aop實現多數據源動態切換

發表於 2016-02-15

一、多數據源動態切換原理

項目中我們經常會遇到多數據源的問題,尤其是數據同步或定時任務等項目更是如此;又例如:讀寫分離數據庫配置的系統。

1、多數據源設置:

1)靜態數據源切換:
一般情況下,我們可以配置多個數據源,然後為每個數據源寫一套對應的sessionFactory和dao層代碼(以hibernate為例,mybatis同理),——我們稱之為靜態數據源配置

2)動態數據源切換:
可看出在Dao層代碼中寫死了兩個SessionFactory,這樣日後如果再多一個數據源,還要改代碼添加一個SessionFactory,顯然這並不符合開閉原則。比較好的做法是,配置多個數據源,只對應一套sessionFactory,數據源之間可以動態切換。

2、動態數據源切換時,如何保證數據庫的事務:

目前事務最靈活的方式,是使用spring的聲明式事務,本質是利用了spring的aop,在執行數據庫操作前後,加上事務處理。

spring的事務管理,是基於數據源的,所以如果要實現動態數據源切換,而且在同一個數據源中保證事務是起作用的話,就需要註意二者的順序問題,即:在事物起作用之前就要把數據源切換回來。

舉一個例子:web開發常見是三層結構:controller、service、dao。一般事務都會在service層添加,如果使用spring的聲明式事物管理,在調用service層代碼之前,spring會通過aop的方式動態添加事務控制代碼,所以如果要想保證事物是有效的,那麽就必須在spring添加事務之前把數據源動態切換過來,也就是動態切換數據源的aop要至少在service上添加,而且要在spring聲明式事物aop之前添加.根據上面分析:

  • 最簡單的方式是把動態切換數據源的aop加到controller層,這樣在controller層裏面就可以確定下來數據源了。不過,這樣有一個缺點就是,每一個controller綁定了一個數據源,不靈活。對於這種:一個請求,需要使用兩個以上數據源中的數據完成的業務時,就無法實現了。
  • 針對上面的這種問題,可以考慮把動態切換數據源的aop放到service層,但要註意一定要在事務aop之前來完成。這樣,對於一個需要多個數據源數據的請求,我們只需要在controller裏面註入多個service實現即可。但這種做法的問題在於,controller層裏面會涉及到一些不必要的業務代碼,例如:合並兩個數據源中的list…
  • 此外,針對上面的問題,還可以再考慮一種方案,就是把事務控制到dao層,然後在service層裏面動態切換數據源。

二、實例1:

本例子中,對不同數據源分包(package)管理,同一包下的代碼使用了同一數據源。
1、寫一個DynamicDataSource類繼承AbstractRoutingDataSource,並實現determineCurrentLookupKey方法

1
2
3
4
5
6
7
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return CustomerContextHolder.getCustomerType();
}
}

2、利用ThreadLocal解決線程安全問題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CustomerContextHolder {
public static final String DATA_SOURCE_A = "dataSource";
public static final String DATA_SOURCE_B = "dataSource2";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setCustomerType(String customerType) {
contextHolder.set(customerType);
}
public static String getCustomerType() {
return contextHolder.get();
}
public static void clearCustomerType() {
contextHolder.remove();
}
}

3、定義一個數據源切面類,通過aop來控制數據源的切換:
1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.JoinPoint;

public class DataSourceInterceptor {

public void setdataSourceMysql(JoinPoint jp) {
DatabaseContextHolder.setCustomerType("dataSourceMySql");
}

public void setdataSourceOracle(JoinPoint jp) {
DatabaseContextHolder.setCustomerType("dataSourceOracle");
}
}

4、在spring的application.xml中配置多個dataSource:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- 數據源1 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="net.sourceforge.jtds.jdbc.Driver"></property>
<property name="url" value="jdbc:jtds:sqlserver://10.82.81.51:1433;databaseName=standards"></property>
<property name="username" value="youguess"></property>
<property name="password" value="youguess"></property>
</bean>
<!-- 數據源2 -->
<bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="net.sourceforge.jtds.jdbc.Driver"></property>
<property name="url" value="jdbc:jtds:sqlserver://10.82.81.52:1433;databaseName=standards"></property>
<property name="username" value="youguess"></property>
<property name="password" value="youguess"></property>
</bean>

<bean id="dataSource" class="com.core.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSourceMySql" value-ref="dataSourceMySql" />
<entry key="dataSourceOracle" value-ref="dataSourceOracle" />
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceMySql" />
</bean>

<!-- 動態數據源切換aop 先與事務的aop -->
<bean id="dataSourceInterceptor" class="com.core.DataSourceInterceptor" />
<aop:config>
<aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">
<aop:pointcut id="dsMysql" expression="execution(* com.service.mysql..*.*(..))" />
<aop:pointcut id="dsOracle" expression="execution(* com.service.oracle..*.*(..))" />
<aop:before method="setdataSourceMysql" pointcut-ref="dsMysql"/>
<aop:before method="setdataSourceOracle" pointcut-ref="dsOracle"/>
</aop:aspect>
</aop:config>

<!-- 事物aop -->
。。。

三、實例2:

該例子,實現了在業務邏輯層控制了mysql的讀寫分離。同樣,使用了spring的aop來動態切換讀和寫的數據源。和上個例子不同之處在於:不同數據源沒有按照包分類管理,而是使用了自定義註解

1、首先配置mysql 的主從復制:
詳情見,這裏。

2、自定義註解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* RUNTIME
* 編譯器將把註釋記錄在類文件中,在運行時 VM 將保留註釋,因此可以反射性地讀取。
* @author yangGuang
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
String value();
}

3、基於spring的aop實現多數據源(讀和寫兩個數據源):
1)寫一個ChooseDataSource: 類繼承AbstractRoutingDataSource,並實現determineCurrentLookupKey方法

1
2
3
4
5
6
7
8
9
10
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class ChooseDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return HandleDataSource.getDataSource();
}

}

2)利用ThreadLocal解決線程安全問題:
1
2
3
4
5
6
7
8
9
10
public class HandleDataSource {
public static final ThreadLocal<String> holder = new ThreadLocal<String>();
public static void putDataSource(String datasource) {
holder.set(datasource);
}

public static String getDataSource() {
return holder.get();
}
}

3)定義一個數據源切面類,通過aop訪問,獲取方法上的自定義註解,然後根據註解內容盡情判斷,動態設置數據源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
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.springframework.stereotype.Component;
[email protected]
[email protected]
public class DataSourceAspect {
[email protected]("execution(* com.apc.cms.service.*.*(..))")
public void pointCut(){};

// @Before(value = "pointCut()")
public void before(JoinPoint point)
{
Object target = point.getTarget();
System.out.println(target.toString());
String method = point.getSignature().getName();
System.out.println(method);
Class<?>[] classz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
.getMethod().getParameterTypes();
try {
Method m = classz[0].getMethod(method, parameterTypes);
System.out.println(m.getName());
if (m != null && m.isAnnotationPresent(DataSource.class)) {
DataSource data = m.getAnnotation(DataSource.class);
HandleDataSource.putDataSource(data.value());
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

4)配置applicationContext.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<!-- 主庫數據源 -->
<bean id="writeDataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://172.22.14.6:3306/cpp?autoReconnect=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="partitionCount" value="4"/>
<property name="releaseHelperThreads" value="3"/>
<property name="acquireIncrement" value="2"/>
<property name="maxConnectionsPerPartition" value="40"/>
<property name="minConnectionsPerPartition" value="20"/>
<property name="idleMaxAgeInSeconds" value="60"/>
<property name="idleConnectionTestPeriodInSeconds" value="60"/>
<property name="poolAvailabilityThreshold" value="5"/>
</bean>

<!-- 從庫數據源 -->
<bean id="readDataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://172.22.14.7:3306/cpp?autoReconnect=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="partitionCount" value="4"/>
<property name="releaseHelperThreads" value="3"/>
<property name="acquireIncrement" value="2"/>
<property name="maxConnectionsPerPartition" value="40"/>
<property name="minConnectionsPerPartition" value="20"/>
<property name="idleMaxAgeInSeconds" value="60"/>
<property name="idleConnectionTestPeriodInSeconds" value="60"/>
<property name="poolAvailabilityThreshold" value="5"/>
</bean>

<!-- transaction manager, 事務管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>


<!-- 註解自動載入 -->
<context:annotation-config />

<!--enale component scanning (beware that this does not enable mapper scanning!)-->
<context:component-scan base-package="com.apc.cms.persistence.rdbms" />
<context:component-scan base-package="com.apc.cms.service">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Component" />
</context:component-scan>

<context:component-scan base-package="com.apc.cms.auth" />

<!-- enable transaction demarcation with annotations -->
<tx:annotation-driven />


<!-- define the SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="typeAliasesPackage" value="com.apc.cms.model.domain" />
</bean>

<!-- scan for mappers and let them be autowired -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.apc.cms.persistence" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

<bean id="dataSource" class="com.apc.cms.utils.ChooseDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- write -->
<entry key="write" value-ref="writeDataSource"/>
<!-- read -->
<entry key="read" value-ref="readDataSource"/>
</map>

</property>
<property name="defaultTargetDataSource" ref="writeDataSource"/>
</bean>

<!-- 激活自動代理功能 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>

<!-- 配置數據庫註解aop -->
<bean id="dataSourceAspect" class="com.apc.cms.utils.DataSourceAspect" />
<aop:config>
<aop:aspect id="c" ref="dataSourceAspect">
<aop:pointcut id="tx" expression="execution(* com.apc.cms.service..*.*(..))"/>
<aop:before pointcut-ref="tx" method="before"/>
</aop:aspect>
</aop:config>
<!-- 配置數據庫註解aop -->

4、測試:

1
2
3
4
5
6
7
8
9
@DataSource("write")  
public void update(User user) {
userMapper.update(user);
}

@DataSource("read")
public Document getDocById(long id) {
return documentMapper.getById(id);
}

  • 測試寫操作:可以通過應用修改數據,修改主庫數據,發現從庫的數據被同步更新了,所以定義的write操作都是走的寫庫
  • 測試讀操作: 後臺修改從庫數據,查看主庫的數據沒有被修改,在應用頁面中刷新,發現讀的是從庫的數據,說明讀寫分離ok。
#javaEE #spring

基於spring的aop實現多數據源動態切換