使用AOP和注解,动态切换数据源

当项目上遇到读写分离,分表分库,版本升级数据导出等多数据源需求的时候,如何实现动态切换数据源?

原理

Spring提供了AbstractRoutingDataSource 这个datasource,看源码发现:

1
2
3
4
5
6
7
8
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}

实现interface DataSource 的getConnection()方法都是使用determineTargetDataSource()方法返回的connection,再观察这个方法发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/

protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}

返回的key是通过determineCurrentLookupKey()这个抽象方法获取的,说明只要重写这个方法返回需要使用的数据源key即可。

思考

但是如何在程序使用过程中自动切换数据源呢?如何方便的自动标记此时需要使用哪个数据源?

实现

spring中的aop功能,可以以切入的方式,在系统使用的过程中动态切换数据源。我们可以对service进行aop实现。但是如何准确方便的标记那些service使用A数据源,哪些service使用B数据源呢? 此时我们可以使用java的annotation 来对使用不用的service进行注解标记。

下面是具体的代码实现:

多数据源配置

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
<!--mybatis 数据库配置 -->
<util:properties id="dbConfig" location="classpath:/config/jdbc.properties"></util:properties>
<beans:bean id="testDataSorce" class="org.apache.commons.dbcp.BasicDataSource"
lazy-init="default" autowire="default">

<beans:property name="driverClassName" value="#{dbConfig['driver']}"></beans:property>
<beans:property name="url" value="#{dbConfig['test.url']}"></beans:property>
<beans:property name="username" value="#{dbConfig['test.user']}"></beans:property>
<beans:property name="password" value="#{dbConfig['test.password']}"></beans:property>
<beans:property name="maxWait" value="#{dbConfig['maxwait']}"></beans:property>
</beans:bean>

<beans:bean id="devDataSorce" class="org.apache.commons.dbcp.BasicDataSource"
lazy-init="default" autowire="default">

<beans:property name="driverClassName" value="#{dbConfig['driver']}"></beans:property>
<beans:property name="url" value="#{dbConfig['dev.url']}"></beans:property>
<beans:property name="username" value="#{dbConfig['dev.user']}"></beans:property>
<beans:property name="password" value="#{dbConfig['dev.password']}"></beans:property>
<beans:property name="maxWait" value="#{dbConfig['maxwait']}"></beans:property>
</beans:bean>

<beans:bean id="dataSource" class="com.yyy.web.mess.datasource.DynamicDataSource" lazy-init="default"
autowire="default">

<beans:property name="targetDataSources">
<beans:map key-type="java.lang.String">
<beans:entry key="testDataSorce" value-ref="testDataSorce"/>
<beans:entry key="devDataSorce" value-ref="devDataSorce"/>
</beans:map>
</beans:property>
<beans:property name="defaultTargetDataSource" ref="testDataSorce"/>
<beans:property name="defaultDataSourceKey" value="testDataSorce"/>
</beans:bean>

此处可以配置多个,且不同类型的数据源,然后需要将数据源加入到dataSource这个bean下的targetDataSources中。

实现

com.yyy.web.mess.datasource.DynamicDataSource继承自前面所提到的AbstractRoutingDataSource,重写determineCurrentLookupKey方法,返回需要使用的数据源key(上面配置的bean id),具体实现如下:

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
package com.yyy.web.mess.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* Created by 程祥 on 15/10/21.
* Function:
*/

public class DynamicDataSource extends AbstractRoutingDataSource{

private String defaultDataSourceKey;

@Override
protected Object determineCurrentLookupKey() {
String dataSource = DataSourceSwitcher.getDataSource();
if(dataSource != null){
return dataSource;
}
return defaultDataSourceKey;
}

public String getDefaultDataSourceKey() {
return defaultDataSourceKey;
}

public void setDefaultDataSourceKey(String defaultDataSourceKey) {
this.defaultDataSourceKey = defaultDataSourceKey;
}
}

使用DataSourceSwitcher单开一个线程,记录当前使用的数据源。

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
package com.yyy.web.mess.datasource;


import org.apache.commons.lang.StringUtils;

/**
* Created by 程祥 on 15/10/21.
* Function:
*/


public class DataSourceSwitcher {


@SuppressWarnings("rawtypes")
private static final ThreadLocal contextHolder = new ThreadLocal();

@SuppressWarnings("unchecked")
public static void setDataSource(String dataSource) {
if(StringUtils.isNotEmpty(dataSource)){
contextHolder.set(dataSource);
}
}

public static String getDataSource() {
String currDataSource = (String) contextHolder.get();
return currDataSource;
}

public static void clearDataSource() {
contextHolder.remove();
}
}

AOP切入

添加一个advice,在方法执行前设置需要使用的数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class AnnotationMultipleDataSourceAdvice implements MethodBeforeAdvice, AfterReturningAdvice,ThrowsAdvice{
public void before(Method method, Object[] args, Object target) throws Throwable {
//获取当前方法使用的annotation,并将annotation的value设置为当前数据源的key
DataSource dataSource = method.getAnnotation(DataSource.class);
String datasourceKey=dataSource!=null ? dataSource.value():null;
DataSourceSwitcher.setDataSource(datasourceKey);
}

//执行完方法后清除thread
public void afterReturning(Object arg0, Method method, Object[] args, Object target) throws Throwable {
DataSourceSwitcher.clearDataSource();
}
//异常后清除thread
public void afterThrowing(Method method, Object[] args, Object target, Exception ex) throws Throwable {
DataSourceSwitcher.clearDataSource();
}
}

annotation的实现

1
2
3
4
5
6
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PACKAGE})
public @interface DataSource {
String value();
}

aop配置文件设置:

1
2
3
4
5
6
<aop:config>
<aop:pointcut id="serviceMethods" expression="execution(* com.yyy.web.mess.service..*(..))"/>
<aop:advisor advice-ref="annotationMultipleDataSourceAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
<beans:bean id="annotationMultipleDataSourceAdvice" class="com.yyy.web.mess.datasource.impl.AnnotationMultipleDataSourceAdvice">
</beans:bean>

至此,所有配置及实现均已完成。

使用

使用的时候只需要在对用的service方法前加上对应的key,mybatis在请求数据源的时候就会根据当前数据源去执行对应操作。如果不设置,则使用的是默认数据源~

1
2
3
4
5
6
7
8
9
10
11
/**
* Created by 程祥 on 15/10/21.
* Function:
*/

public interface OrderDevService {
@DataSource("devDataSorce")
int insertDatas2Dev(List<Order> orders) throws Exception;

@DataSource("devDataSorce")
List<Order> selectTest() throws Exception;
}

总结

通过annotation能够方便且清晰的告知当前需要使用哪个数据源,使用aop通过解析annotation的value,动态的切换数据源。这样在需要多数据源系统中能够方便高效的编写业务逻辑代码。