PRELOADER

当前文章 : 《实现动态切换数据源》

5/5/2019 —— 

前言

  因为公司在给客户做的党建项目中有一个数据报送的功能,其中涉及到了数据源切换。功能全称《中组部数据报送》,涵盖的子模块:数据源管理、数据报送、报送日志。
功能的描述:把系统中的党组织信息、党员信息、行政单位等信息查出来经过处理,也就是把一些信息转换成特定的编码、代号。本来想着在配置文件中配置两个数据源,当使用数据报送模块时切换一下数据源,但是数据源是从库里面拿的,由操作者选择,所以报送的数据源是动态的。
所以在配置文件中再配一个数据源是无法实现的,因为Spring初始化后另一个数据源就是死的了,与需求不符。最后使用spring的AbstractRoutingDataSource实现数据源切换。

实现步骤

修改Spring配置文件

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
<!-- 数据源配置, 使用 BoneCP 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="${jdbc.driver}" />

<!-- 基本属性 url、user、password -->
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />

<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.pool.init}" />
<property name="minIdle" value="${jdbc.pool.minIdle}" />
<property name="maxActive" value="${jdbc.pool.maxActive}" />

<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000" />

<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />

<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />

<property name="validationQuery" value="${jdbc.testSql}" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />

<!-- 打开PSCache,并且指定每个连接上PSCache的大小(Oracle使用)
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->

<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat" />
</bean>


<bean id="dynamicDataSource" class="com.xxx.common.dataSource.DruidDynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSource" value-ref="dataSource"/>
<!-- <entry key="dataDeliveryDataSource" value-ref="dataDeliveryDataSource"/> -->
</map>
</property>
<!--默认数据源-->
<property name="defaultTargetDataSource" ref="dataSource"/>
</bean>

编写DBContextHolder类

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
import java.util.HashMap;
import java.util.Map;

/**
* @author: lcy
* @CreateTime: 2018年7月27日 下午4:59:08
* @说明:用于数据源操作
**/
public class DBContextHolder {
/** 数据源的KEY */
public static final String DATASOURCE_KEY = "DATASOURCE_KEY";
/** 数据源的URL */
public static final String DATASOURCE_URL = "DATASOURCE_URL";
/** 数据源的驱动 */
public static final String DATASOURCE_DRIVER = "DATASOURCE_DRIVER";
/** 数据源的用户名 */
public static final String DATASOURCE_USERNAME = "DATASOURCE_USERNAME";
/** 数据源的密码 */
public static final String DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD";

private static final ThreadLocal<Map<String, Object>> contextHolder = new ThreadLocal<Map<String, Object>>();

/**
* 切换数据源
* @param dataSourceConfigMap
*/
public static void setDBType(Map<String, Object> dataSourceConfigMap) {
contextHolder.set(dataSourceConfigMap);
}

/**
* 获取数据源
* @return
*/
public static Map<String, Object> getDBType() {
Map<String, Object> dataSourceConfigMap = contextHolder.get();
if (dataSourceConfigMap == null) {
dataSourceConfigMap = new HashMap<String, Object>();
}
return dataSourceConfigMap;
}

/**
* 清除数据源
*/
public static void clearDBType() {
contextHolder.remove();
}
}

编写AbstractDynamicDataSource类继承Spring的AbstractRoutingDataSource

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* @author: lcy
* @CreateTime: 2018年7月27日 下午4:59:51
* @说明:用于决定使用哪一个数据源。
* 实现ApplicationContextAware接口,用于在Spring加载完成后,注入Spring上下文对象,用于获取Bean。
**/
public abstract class AbstractDynamicDataSource<T extends DataSource> extends AbstractRoutingDataSource
implements ApplicationContextAware {

/** 日志 */
protected Logger logger = LoggerFactory.getLogger(getClass());
/** 默认的数据源KEY */
protected static final String DEFAULT_DATASOURCE_KEY = "dataSource";

/** 数据源KEY-VALUE键值对 */
public Map<Object, Object> targetDataSources;

/** spring容器上下文 */
private static ApplicationContext ctx;

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ctx = applicationContext;
}

public static ApplicationContext getApplicationContext() {
return ctx;
}

public static Object getBean(String name) {
return ctx.getBean(name);
}

/**
* @param targetDataSources
* the targetDataSources to set
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
super.setTargetDataSources(targetDataSources);
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}

/**
* 创建数据源
* @param driverClassName 数据库驱动名称
* @param url 连接地址
* @param username 用户名
* @param password 密码
* @return 数据源{@link T}
*/
public abstract T createDataSource(String driverClassName, String url, String username, String password);

/**
* 设置系统当前使用的数据源
* 数据源为空或者为0时,自动切换至默认数据源,即在配置文件中定义的默认数据源
*/
@Override
protected Object determineCurrentLookupKey() {
Map<String, Object> configMap = DBContextHolder.getDBType();
logger.info("【当前数据源配置为:{}】", configMap);
if (MapUtils.isEmpty(configMap)) {
// 使用默认数据源
return DEFAULT_DATASOURCE_KEY;
}
// 判断数据源是否需要初始化
this.verifyAndInitDataSource();
logger.info("【切换至数据源:{}】", configMap);
return configMap.get(DBContextHolder.DATASOURCE_KEY);
}

/**
* 判断数据源是否需要初始化
*/
private void verifyAndInitDataSource() {
Map<String, Object> configMap = DBContextHolder.getDBType();
Object obj = this.targetDataSources.get(configMap.get(DBContextHolder.DATASOURCE_KEY));
if (obj != null) {
return;
}
logger.info("【初始化数据源】");
T datasource = this.createDataSource(configMap.get(DBContextHolder.DATASOURCE_DRIVER).toString(),
configMap.get(DBContextHolder.DATASOURCE_URL).toString(),
configMap.get(DBContextHolder.DATASOURCE_USERNAME).toString(),
configMap.get(DBContextHolder.DATASOURCE_PASSWORD).toString());
this.addTargetDataSource(configMap.get(DBContextHolder.DATASOURCE_KEY).toString(), datasource);
}

/**
* 往数据源key-value键值对集合添加新的数据源
* @param key 新的数据源键
* @param dataSource 新的数据源
*/
private void addTargetDataSource(String key, T dataSource) {
this.targetDataSources.put(key, dataSource);
super.setTargetDataSources(this.targetDataSources);
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}

}

编写DruidDynamicDataSource类

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
92
import java.sql.SQLException;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidDataSource;
/**
*@author: lcy
*@CreateTime: 2018年7月27日 下午5:00:43
*@说明:实现创建数据源的方法
**/
public class DruidDynamicDataSource extends AbstractDynamicDataSource<DruidDataSource> {

private boolean testWhileIdle = true;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;

// 是否打开连接泄露自动检测
private boolean removeAbandoned = false;
// 连接长时间没有使用,被认为发生泄露时长
private long removeAbandonedTimeoutMillis = 300 * 1000;
// 发生泄露时是否需要输出 log,建议在开启连接泄露检测时开启,方便排错
private boolean logAbandoned = false;

// 只要maxPoolPreparedStatementPerConnectionSize>0,poolPreparedStatements就会被自动设定为true,使用oracle时可以设定此值。
// private int maxPoolPreparedStatementPerConnectionSize = -1;

// 配置监控统计拦截的filters
private String filters; // 监控统计:"stat" 防SQL注入:"wall" 组合使用: "stat,wall"
private List<Filter> filterList;

/*
* 创建数据源
*/
@Override
public DruidDataSource createDataSource(String driverClassName, String url, String username,
String password) {
DruidDataSource parent = (DruidDataSource) super.getApplicationContext().getBean(
DEFAULT_DATASOURCE_KEY);
DruidDataSource ds = new DruidDataSource();
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
ds.setDriverClassName(driverClassName);
ds.setInitialSize(parent.getInitialSize());
ds.setMinIdle(parent.getMinIdle());
ds.setMaxActive(parent.getMaxActive());
ds.setMaxWait(parent.getMaxWait());
ds.setTimeBetweenConnectErrorMillis(parent.getTimeBetweenConnectErrorMillis());
ds.setTimeBetweenEvictionRunsMillis(parent.getTimeBetweenEvictionRunsMillis());
ds.setMinEvictableIdleTimeMillis(parent.getMinEvictableIdleTimeMillis());

ds.setValidationQuery(parent.getValidationQuery());
ds.setTestWhileIdle(testWhileIdle);
ds.setTestOnBorrow(testOnBorrow);
ds.setTestOnReturn(testOnReturn);

ds.setRemoveAbandoned(removeAbandoned);
ds.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis);
ds.setLogAbandoned(logAbandoned);
ds.setMaxPoolPreparedStatementPerConnectionSize(parent
.getMaxPoolPreparedStatementPerConnectionSize());

if (StringUtils.isNotBlank(filters))
try {
ds.setFilters(filters);
} catch (SQLException e) {
throw new RuntimeException(e);
}

addFilterList(ds);
return ds;
}

private void addFilterList(DruidDataSource ds) {
if (filterList != null) {
List<Filter> targetList = ds.getProxyFilters();
for (Filter add : filterList) {
boolean found = false;
for (Filter target : targetList) {
if (add.getClass().equals(target.getClass())) {
found = true;
break;
}
}
if (!found)
targetList.add(add);
}
}
}
}

使用

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
   @ResponseBody
@RequestMapping(value = {"dataDelivery"})
public HashMap<String, Object> dataDelivery(String id) {
HashMap<String, Object> result = new HashMap<String, Object>();
List<ReturnMessage> list = new ArrayList<ReturnMessage>();
try {
DeliveryDataSources dataSources = deliveryDataSourcesService.get(id);
//检测数据源是否正确
if(ConnectionTest.getConnection(dataSources)) {//连接成功-开始创建数据源
//获取处理后的数据
DataList dataList =findDataService.findData();

String key = "DataSources"+System.identityHashCode(d);
Map<String, Object> map = new HashMap<String, Object>();
map.put(DBContextHolder.DATASOURCE_KEY, key);
map.put(DBContextHolder.DATASOURCE_DRIVER, dataSources.getDatabaseDriver());
map.put(DBContextHolder.DATASOURCE_URL,dataSources.getUrl());
map.put(DBContextHolder.DATASOURCE_USERNAME, dataSources.getDatabaseUsername());
map.put(DBContextHolder.DATASOURCE_PASSWORD,dataSources.getDatabasePassword());
//切换数据源
DBContextHolder.setDBType(map);
//开始报送
list = dataDeliveryService.batchSaveDataList(dataList);

//清除数据源 切换回原数据源
DBContextHolder.clearDBType();
result.put("status", "1");//报送成功
result.put("list", list);
}else {//数据源错误
result.put("status", "2");//报送数据源错误
}
} catch (Exception e) {
result.put("status", "3");//报送失败
System.out.println("报送失败");
e.printStackTrace();
}
return result;
}

效果(其实通过图片看不出来什么)

效果

效果

效果