Springboot 2.0.0 + mybatis实现多数据源切换

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

为什么要使用多数据源切换?

由于业务需要,对基础服务模块升级。独立出来一个服务,这个服务的用户数据在MySQL库中,主体分别存放在两个不同的Oracle库中,权限数据又放在另外一个Oracle库中,数据来源比较复杂,所以考虑使用多数据源切换来实现。

实现思路:

假设前台发送两条数据请求,需要获取当前用户基本信息和用户的权限信息。由于用户数据和权限数据分属不同类型的数据库(MySQL和Oracle),那么在执行数据查询的时候如何告诉程序去选择对应的数据库查询呢?

通过对Spring的了解,我们都知道Spring AOP(面向切面编程的方式),那么是否可以定义一个切点,在切点处进行数据源的切换呢?答案是肯定的。

既然有了思路,接下来我们进行具体的实现。

首先,多数据源作为配置肯定用常量来表示比较合适。

那么,我们先定义数据源常量的配置,对不同的数据源进行常量标记(这里就简单用两个数据源做例子)

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum DataSourceEnum {
DB1("mysql_DB"), DB2("oracle_DB");
 
private String value;
 
DataSourceEnum(String value) {
this.value = value;
}
 
public String getValue() {
return value;
}
}

然后再准备一个自定义注解,作为拦截标记来方便我们在切入点处进行数据源切换,设置默认的数据库类型为:mysql_DB

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
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
 
DataSourceEnum value() default DataSourceEnum.DB1;
}
接下来写一个 DataSourceContextHolder 类,对线程(上下文)使用的数据源进行设置、获取以及移除操作:
 
public class DataSourceContextHolder {
 
private static final ThreadLocal contextHolder = new InheritableThreadLocal();
 
/**
* 设置数据源
* @param db
*/
public static void setDataSource(String db){
contextHolder.set(db);
}
 
/**
* 取得当前数据源
* @return
*/
public static String getDataSource(){
return contextHolder.get();
}
 
/**
* 清除上下文数据
*/
public static void clear(){
contextHolder.remove();
}
}

之后,创建一个数据源切换类用来封装数据源选择逻辑,这个类只需要继承 AbstractRoutingDataSource 类,进而实现它的determineCurrentLookupKey()方法。determineCurrentLookupKey()中封装了数据源选择的实现逻辑。

1
2
3
4
5
6
public class MultipleDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}

数据源有了、选择器有了,接下来就需要把我们配置在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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Configuration
@MapperScan("com.lzq.api.dao.*")
public class MyBatiesPlusConfiguration {
 
@Bean(name = "mysql_DB")
@ConfigurationProperties(prefix = "spring.datasource.druid.trans" )
public DataSource trans() {
return DruidDataSourceBuilder.create().build();
}
 
@Bean(name = "oracle_DB")
@ConfigurationProperties(prefix = "spring.datasource.druid.xzsp" )
public DataSource xzsp() {
return DruidDataSourceBuilder.create().build();
}
 
/**
* 动态数据源配置
* @return
*/
@Bean
@Primary
public DataSource multipleDataSource(@Qualifier("mysql_DB") DataSource db1, @Qualifier("oracle_DB") DataSource db2) {
  //在AbstractRoutingDataSource 的源码中可以发现targetDataSources使用Map键值对来保存的
MultipleDataSource multipleDataSource = new MultipleDataSource();
Map targetDataSources = new HashMap();
  //将我们配置的数据源放入map集合中
targetDataSources.put(DataSourceEnum.DB1.getValue(), db1);
targetDataSources.put(DataSourceEnum.DB2.getValue(), db2);
//将数据源放入选择器中
multipleDataSource.setTargetDataSources(targetDataSources);
//设置选择器的默认值
multipleDataSource.setDefaultTargetDataSource(db1);
return multipleDataSource;
}
 
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(multipleDataSource(mysql_DB(),oracle_DB()));
  //扫描持久层
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml"));
 
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory.getObject();
}
}

在所有准备工作都做好之后,就是具体的实现了。上文说过,我们利用AOP的切点来做数据源注入切换。

首先定义切点:

1
2
3
@Pointcut("@annotation(com.hd.xzsp.api.config.anno.DataSource)")
public void pointCut(){
}

切点定义好之后,就可以在持久化操作之前进行数据源切换

1
2
3
4
@Before("pointCut() && @annotation(dataSource)")
public void doBefore(DataSource dataSource){
DataSourceContextHolder.setDataSource(dataSource.value().getValue());
}

注入完成之后移除上下文中的数据源

1
2
3
4
@After("pointCut()")
public void doAfter(){
DataSourceContextHolder.clear();
}

完整代码(order值越小,该组件则最早被spring加载):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
@Aspect
@Order(-1)
public class DataSourceAspect {
 
@Pointcut("@annotation(com.hd.xzsp.api.config.anno.DataSource)")
public void pointCut(){
}
 
@Before("pointCut() && @annotation(dataSource)")
public void doBefore(DataSource dataSource){
DataSourceContextHolder.setDataSource(dataSource.value().getValue());
}
 
@After("pointCut()")
public void doAfter(){
DataSourceContextHolder.clear();
}
}

application.yml数据源配置部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
datasource:
druid:
trans:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/lzq-test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
password: 123456
xzsp:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.1:1521:ORCL
username: root
password: 123456

测试代码这里就不写了,主要是思路和实现的步骤。