Spring Boot + Druid 多数据源绑定

Spring Boot + Druid 多数据源绑定

版本环境:Spring Boot 2.0.6

参考文档地址:
https://blog.csdn.net/cllaure/article/details/81509303

1. 项目结构

列举个几个比较重要的文件,其他的文件进行了省略

  • analysis
    • AnalysisApplication.java
    • SpringUtil.java
  • dao
    • OperateLogMapper.java
  • datasource
    • DynamicDataSource.java
    • DynamicDataSourceAspect.java
    • DynamicDataSourceContextHolder.java
    • DynamicDataSourceRegister.java
    • TargetDataSource.java
  • entity
  • service
    • AnalysisService.java
  • utils

2. 关于启动类 AnalysisApplication

因为我想要的是在启动该项目时,不通过访问 url 的方式来激活某个接口,而是让程序自动来执行某个方法,所以需要在启动类中使用 SpringUtil.getApplicationContext() 来获取 Spring 上下文,从而将你需要的那个类注入进来,并执行该来的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
@Import(DynamicDataSourceRegister.class) // 重点!!!用来覆写 Spring 默认创建数据库连接方法
@ComponentScan("com.bigdata.*") // Service层的注解是@component,但是有可能会访问不到,在这里通过注解的方式直接将整个目录都引入进来
@MapperScan("com.bigdata.dao")
public class AnalysisApplication {
public static void main(String[] args) {
SpringApplication.run(AnalysisApplication.class, args);

// 当 Spring 启动后会获取到 analysis 实例,并执行 appStart() 方法
ApplicationContext context = SpringUtil.getApplicationContext();
AnalysisService analysis = context.getBean(AnalysisService.class);
analysis.appStart();
}
}

3. 关于 SpringUtil

需要注意的是,此类需要放到启动类同包或者子包下才能被扫描,否则失效。

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
@Component
public class SpringUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext = null;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null){
SpringUtil.applicationContext = applicationContext;
}
}

//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}

//通过name获取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);

}

//通过class获取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}

//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}

4. 多数据源连接初始化

首先创建一个 datasource 的文件夹,该文件夹下需要的文件有:

  • DynamicDataSource.java
  • DynamicDataSourceAspect.java
  • DynamicDataSourceContextHolder.java
  • DynamicDataSourceRegister.java
  • TargetDataSource.java

如果是想通过注解的方法来切换数据源,那么需要用到 DynamicDataSourceAspect.javaTargetDataSource.java,反之如果你的数据源连接特别多,要通过传参来切换数据源,那么就不需要这两个文件。

下面我会按照多数据源连接为例,改写文档上的代码。

4.1 DynamicDataSource.java

这个文件的目的是覆写 determineCurrentLookupKey() 方法,返回的值是保存在 DynamicDataSourceContextHolder 类中当前数据库连接

1
2
3
4
5
6
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}

4.2 DynamicDataSourceContextHolder.java

该文件是用来切换数据源,通过 setDataSourceType(String dataSourceType) 方法来切换到指定的数据库连接,其中 dataSourceType 其实是在初始化数据库连接时的别名。

dataSourceIds 里保存了所有数据库连接的别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static List<String> dataSourceIds = new ArrayList<>();

public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}

public static String getDataSourceType() {
return contextHolder.get();
}

// 恢复至默认的数据库
public static void clearDataSourceType() {
contextHolder.remove();
}

/**
* 判断指定DataSrouce当前是否存在
*/
public static boolean containsDataSource(String dataSourceId) {
return dataSourceIds.contains(dataSourceId);
}
}

4.3 DynamicDataSourceRegister.java

该文件用来初始化数据源连接,并注入到 Bean 中。

Spring 启动前,会自动执行 setEnvironment(Environment env) 方法,在这个方法体通过读取配置文件,来初始化数据库连接。由于我们需要通过别名来切换数据库连接,所以如果在库名唯一的情况下,我们可以通过数据库名来作为别名。

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
/**
* 动态数据源注册
* 启动动态数据源请在启动类中 添加 @Import(DynamicDataSourceRegister.class)
*/

public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);

// 如配置文件中未指定数据源类型,使用该默认值
private static final Object DATASOURCE_TYPE_DEFAULT = "com.alibaba.druid.pool.DruidDataSource";

// 数据源
private DataSource defaultDataSource;
private Map<String, DataSource> customDataSources = new HashMap<>();

//加载多数据源配置
@Override
public void setEnvironment(Environment env) {
// 读取配置文件获取更多数据源
PropertiesUtil props = new PropertiesUtil("relations.properties");
PropertiesEntity propsEntity = PropertiesEntity.newInstance();
...... 解析配置文件并赋值 ......

System.out.println("开始初始化动态数据源~~~~");
initCustomDataSources(propsEntity);
}

//初始化更多数据源
private void initCustomDataSources(PropertiesEntity propsEntity) {
Map<String, Object> dsMap = new HashMap<>();

// 通过循环来实现多个数据库连接的初始化
dsMap.put("driver-class-name", propsEntity.getDriveClass());
dsMap.put("url", propsEntity.getUrl());
dsMap.put("username", propsEntity.getUsername());
dsMap.put("password", propsEntity.getPassword());
DataSource ds = buildDataSource(dsMap);

// 假设库名为 "db1", 将其设置为默认数据库连接,当然也可以不设置,这个无所谓,在 `registerBeanDefinitions` 方法中将默认数据库连接的别名绑定为 default
if(dbName.equals("db1")){
defaultDataSource = buildDataSource(dsMap);
}else{
customDataSources.put(dbName, ds);
}
}

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
// 将主数据源添加到更多数据源中
targetDataSources.put("default", defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("default");
// 添加更多数据源
targetDataSources.putAll(customDataSources);
for (String key : customDataSources.keySet()) {
DynamicDataSourceContextHolder.dataSourceIds.add(key);
}

// 创建DynamicDataSource
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
mpv.addPropertyValue("targetDataSources", targetDataSources);
registry.registerBeanDefinition("default", beanDefinition);

logger.info("Dynamic DataSource Registry");

}

//创建DataSource
@SuppressWarnings("unchecked")
public DataSource buildDataSource(Map<String, Object> dsMap) {
try {
Object type = dsMap.get("type");
if (type == null)
type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource

Class<? extends DataSource> dataSourceType;
dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);

String driverClassName = dsMap.get("driver-class-name").toString();
String url = dsMap.get("url").toString();
String username = dsMap.get("username").toString();
String password = dsMap.get("password").toString();

DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(dataSourceType);
return factory.build();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}

4.4 DynamicDataSourceAspect.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
/**
* 切换数据源Advice
*/
@Aspect
@Order(-1)// 保证该AOP在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

@Before("@annotation(ds)")
public void changeDataSource(JoinPoint point, TargetDataSource ds) {
String dsId = ds.name();
if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
logger.error("数据源[{}]不存在,使用默认数据源 > {}", ds.name(), point.getSignature());
}else {
logger.debug("Use DataSource : {} > {}", dsId, point.getSignature());
DynamicDataSourceContextHolder.setDataSourceType(dsId);
}

}
@After("@annotation(ds)")
public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
logger.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature());
DynamicDataSourceContextHolder.clearDataSourceType();
}
}

4.5 TargetDataSource.java

注解文件

1
2
3
4
5
6
7
// 在方法上使用,用于指定使用哪个数据源
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name();
}

5. 方法实现

如果是通过注解实现,可以在此文件中的方法或者dao层中的方法上使用注解,并通过对 name 进行赋值来切换数据库,如果没有值的话使用的是默认数据库连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class AnalysisService {

@Autowired
OperateLogMapper opLogMapper;

// @TargetDataSource(name="")
public void appStart() {
// 切换到别名为 db1 的数据库连接
DynamicDataSourceContextHolder.setDataSourceType("db1");
... 数据库操作 ...
// 恢复至默认连接
DynamicDataSourceContextHolder.clearDataSourceType();

// 通过别名切换到默认连接,如果不通过 set 方法直接进行数据库操作,也是在默认连接进行的
DynamicDataSourceContextHolder.setDataSourceType("default");
... 数据库操作 ...
}
}

6. 关于 MyBatis.xml

使用 MyBatis 来编写 sql 语句时,将表名通过变量传递。此时如果用 #{0},会报错,因为 # 会把里面的参数自动加上单引号然后拼接到字符串上,如果是对于类似 where name = '张三' 是没有问题的,但是对于 select * from 'tableName' 是有问题的。

有两个解决方法,一个是将需要传递的参数打包成 HashMap,在赋值的时候使用 select * from ${_key}, 这样就可以动态赋值了。第二个方法是传递N个参数,使用 select * from ${param1} 来进行赋值。