提到Spring的动态数据源重点在于对AbstractRoutingDataSource
实现及工作原理的认知。
先从Spring的DataSource开始说起,而说到DataSource,用最原始的jdbc来说明可能会更容易理解了。
在使用jdbc时,要进行一个sql操作,我们需要这样的一个链路:
Statement
<- Connection
<- DataSource
也就是根源上就是获取一个DataSource
,这就是关联到一个具体源的对象。那么切换源,从原理上来讲就是切换这个DataSource
对象了。
那么,下一步,来看看这个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
|
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
...
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
...
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
...
/**
* 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;
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
|
现在来进行一一的解读。
afterPropertiesSet
是啥?这就不细说了。看源码就是解析targetDataSources
然后放到resolvedDataSources
;
getConnection
获取一个Connection
对象,这里和jdbc就是一个玩意了;
determineTargetDataSource
确定目标的DataSource
,在这个方法中有这样一行Object lookupKey = determineCurrentLookupKey();
,而这个就是调用了一个determineCurrentLookupKey
方法;
determineCurrentLookupKey
,一个抽象方法,返回一个当前处理的源的id
或者说key
。
从源码中看出,AbstractRoutingDataSource
的工作原理,大致就是,从一个Map
的源集合中,可以一个当前处理的源Key
来切换选择对应的DataSource
,从而完成对应源的切换。
下面,从具体实现上来做解析,首先看看AbstractRoutingDataSource
的定义
1
|
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean
|
这是一个抽象class,那么要用他,必然要实现一个实例class,如下。
1
|
public class DynamicDataSource extends AbstractRoutingDataSource
|
那么重点就是对抽象方法determineCurrentLookupKey
的处理和源Map
的构建和操作了。
要知道,在实际应用中,应用都是工作在多线程环境的,那么对源的切换就变成了在多线程环境下的切换,而要保存多线程环境下的切换安全,我们自然而然想到了ThreadLocal
。是的,我们就是借助它,来实现和完成在不同源的CurrentLookupKey
的管理。
先看看ThreadLocal
的处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class DynamicDataSourceContextHolder {
private DynamicDataSourceContextHolder() {
}
private static final ThreadLocal<String> dynamicDataSourceContextHolder = new ThreadLocal<String>();
public static void setCurrentDataSource(String dataSourceKey) {
dynamicDataSourceContextHolder.set(dataSourceKey);
}
public static String getCurrentDataSource() {
return dynamicDataSourceContextHolder.get();
}
public static void clearCurrentDataSource() {
dynamicDataSourceContextHolder.remove();
}
}
|
实现很简单,就是使用ThreadLocal
来管理key,这里的key是String
类型的。在切换时,调用setCurrentDataSource
,使用完成在调用clearCurrentDataSource
,使用getCurrentDataSource
来获取当前设置的key。
接着看在DynamicDataSource
中的处理:
1
2
3
4
|
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getCurrentDataSource();
}
|
这样整个都串起来了:
实现继承AbstractRoutingDataSource
的类DynamicDataSource
,实现determineCurrentLookupKey
方法,这个方法从ThreadLocal
中拿到当前线程处理的源的key信息,在AbstractRoutingDataSource
的determineTargetDataSource
中根据这个key拿到在源Map
中已经配置好的源DataSource
,拿到了DataSource
,再要拿到Connection
还会远吗?^_^
剩下的就是构造一个有源Map
管理(put,get)的DynamicDataSource
并注入到IOC
,就ok咯。
最后,可以在DynamicDataSource
加入一个销毁DataSource
的方法,避免应用关闭后,仍然占用资源。
1
2
3
4
|
@PreDestroy
public void closeDataSources(){
// 从Map<Object, Object> targetDataSources遍历获取对应的DataSource并关闭
}
|