背景

  • 同一个项目数据操作有时会涉及到多个数据库,所以需要在后台配置多个数据源,通过特定的设置选择指定的数据库

目的

  • 可以配置多个数据源,项目默认使用master数据源,当有新的数据源需求时,可以通过注解的形式动态切换数据源

实现

  • 源码: https://github.com/songshuiyang/iframe

application.yml 配置数据源

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
spring:
application:
name: iframe
# 主数据库
master:
datasource:
use-jndi: false
jndi-name: jdbc/datasource
url: jdbc:mysql://127.0.0.1:3306/iframe?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
username: root
password: root
driver-class: com.mysql.jdbc.Driver
initial-size: 0
min-idle: 10
max-active: 100
max-wait: 20000
# 从数据库
slave:
datasource:
use-jndi: false
jndi-name: jdbc/datasource
url: jdbc:mysql://127.0.0.1:3306/iframe?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
username: root
password: root
driver-class: com.mysql.jdbc.Driver
initial-size: 0
min-idle: 10
max-active: 100
max-wait: 20000

Config.java 解析数据源配置

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* 继承TransactionManagementConfigurer可以自定义事务管理器
* @author songsy
* @Date 2018/11/7 17:09
*/
@Configuration
@EnableTransactionManagement
public class Config implements TransactionManagementConfigurer {

private static final Logger logger = LoggerFactory.getLogger(Config.class);

@Autowired
Environment env;

/**
* 实现接口 TransactionManagementConfigurer 方法,其返回值代表在拥有多个事务管理器的情况下默认使用的事务管理器
*
* @return
*/
@Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return transactionManager();
}

/**
* 数据源
*
* @return
*/
@Bean(name = "dataSource")
public DynamicDataSource dataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
for (String prefix : DATA_SOURCE_PREFIX) {
targetDataSources.put(prefix, createDataSource(prefix));
}
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(targetDataSources.get(MASTER_DATA_SOURCE_PREFIX));
return dynamicDataSource;
}

/**
* 创建事务管理器
*
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager txManager = new DataSourceTransactionManager();
txManager.setDataSource(dataSource());
return txManager;
}

/**
* 创建数据源
*
* @param prefix
* @return
*/
private DataSource createDataSource(String prefix) {
// 是否使用数据源
boolean useJndi = env.getProperty(prefix + "." + "datasource.use-jndi", Boolean.class, false);
// 数据源名称
String jndiName = env.getProperty(prefix + "." + "datasource.jndi-name", "");
// 数据库链接
String url = env.getProperty(prefix + "." + "datasource.url", "");
String username = env.getProperty(prefix + "." + "datasource.username", "");
String password = env.getProperty(prefix + "." + "datasource.password", "");
String driverClass = env.getProperty(prefix + "." + "datasource.driver-class", "");
// 数据源默认初始链接数
int initialSize = env.getProperty(prefix + "." + "datasource.initial-size", Integer.class,
DataSouceConstant.DEFAULT_DATASOURCE_INIT_SIZE);
// 数据源最大连接数
int maxActive = env.getProperty(prefix + "." + "datasource.max-active", Integer.class,
DataSouceConstant.DEFAULT_DATASOURCE_MAX_ACTIVE);
// 数据源最小连接数
int minIdle = env.getProperty(prefix + "." + "datasource.min-idle", Integer.class,
DataSouceConstant.DEFAULT_DATASOURCE_MIN_IDLE);
// 配置获取连接等待超时的时间
int maxWait = env.getProperty(prefix + "." + "datasource.max-wait", Integer.class,
DataSouceConstant.DEFAULT_DATASOURCE_MAX_WAIT);
if (useJndi) {
try {
logger.debug("get datasource from jndi - [{}].", jndiName);
Context context = new InitialContext();
DataSource dataSource = (DataSource) context.lookup(jndiName);
return dataSource;
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
} else {
logger.debug("create druid datasource.");
logger.debug("url - {}.", url);
logger.debug("username - {}.", username);
logger.debug("password - {}.", password);
logger.debug("driverClass - {}.", driverClass);
logger.debug("initialSize - {}.", initialSize);
logger.debug("maxActive - {}.", maxActive);
logger.debug("minIdle - {}.", minIdle);

try {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(url);
datasource.setDriverClassName(driverClass);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);
datasource.setMaxWait(maxWait);
datasource.setFilters("stat,slf4j");
datasource.setProxyFilters(getDruidFilters());
return datasource;
} catch (Exception e) {
}

}
return null;
}

public List<Filter> getDruidFilters() {
Slf4jLogFilter slf4jLogFilter = new Slf4jLogFilter();
slf4jLogFilter.setDataSourceLogEnabled(false);
slf4jLogFilter.setStatementLogEnabled(false);
slf4jLogFilter.setStatementExecutableSqlLogEnable(true);
slf4jLogFilter.setResultSetLogEnabled(false);
slf4jLogFilter.setResultSetCloseAfterLogEnabled(false);
slf4jLogFilter.setConnectionLogEnabled(false);
List<Filter> filters = new ArrayList<>();
filters.add(new StatFilter());
filters.add(slf4jLogFilter);
return filters;
}
}

新增 @BindingDataSources 注解,通过该注解实现数据源切换

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 绑定数据源注解
* @author songsy
* @Date 2018/11/7 17:33
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BindingDataSources {

String value() default "master";

}

新增 DynamicDataSource.java 实现切换数据源

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 继承AbstractRoutingDataSource实现determineCurrentLookupKey方法,该方法可以实现数据库的动态切换
* @author songsy
* @Date 2018/11/7 17:17
*/
public class DynamicDataSource extends AbstractRoutingDataSource {

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

新增 DynamicDataSourceHolder.java 保存当前线程绑定的数据源信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 保存当前线程绑定的数据源信息
* @author songsy
* @Date 2018/11/7 17:18
*/
public class DynamicDataSourceHolder {

private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<>();

public static void setDataSource(String dataSource) {
Assert.notNull(dataSource, "dataSource cannot be null");
dataSourceHolder.set(dataSource);
}

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

public static void removeDataSource() {
dataSourceHolder.remove();
}

}

通过 DynamicDataSourceAspect.java AOP切面来获取数据源注解信息并设置到 private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<>(); 变量中,那么determineCurrentLookupKey 方法就可以根据当前线程数据源key值去动态切换数据源

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
/**
* 配置数据源切面
* @author songsy
* @Date 2018/11/7 17:35
*/
@Aspect
@Order(-1)// 保证该AOP在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {

private final static Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

@Pointcut("@annotation(com.songsy.iframe.core.persistence.datasource.annotation.BindingDataSources)")
public void pointcut() {
}

@Before("pointcut() && @annotation(bindingDataSources)")
public void setDynamicDataSource(JoinPoint point, BindingDataSources bindingDataSources) {
Object target = point.getTarget();
Method method = ((MethodSignature) point.getSignature()).getMethod();
logger.debug("切换数据源: 类名 - {}", target.getClass().getCanonicalName());
logger.debug("切换数据源: 方法名 - {}", method.getName());
String key = bindingDataSources.value();
DynamicDataSourceHolder.setDataSource(key);
logger.debug("切换数据源:[{}] 数据源切换成功.", DynamicDataSourceHolder.getDataSource());
}

@After("pointcut()")
public void clearDynamicDataSource(JoinPoint point) {
DynamicDataSourceHolder.removeDataSource();
}
}