SpringBoot配置动态数据源 原理+实战
若没空探究原理可直接跳转到“实现方式:注解+切面”目录
数据源切换方法
Spring对数据源的管理类似于策略模式,不懂策略模式也没关系,其实就是有一个全局的键值对,类型是Map<String, DataSource>
。当JDBC
操作数据库之时,会根据不同的key值选择不同的数据源。而这个key值可以放到方法的注解里。
所以切换数据源的思路就是让JDBC
在获取数据源时根据key获取到要切换的数据源。
JDBC
提供了AbstractRoutingDataSource
抽象类,类名意思是数据源路由,该类提供了一个抽象方法determineCurrentLookupKey()
,切换数据源时JDBC
会调用这个方法获取数据源的key,所以只需要实现该方法,改变该方法中返回的key值即可。
源码解读
1.从类关系图中可以看出AbstractRoutingDataSource
类实现了DataSource
接口,后者有两个getConnection()
方法,即获取DB连接的作用。
2.AbstractRoutingDataSource
实现了这两个方法
其中determineTargetDataSource()
方法的作用就是获取实际的数据源,其内部调用了determineCurrentLookupKey()
方法,取到当前设定的key,通过key在上下文this.resolvedDataSources
属性中尝试获取DataSource对象,这个对象即当前连接的数据源
3.那么this.resolvedDataSources
在哪里维护呢? 继续在AbstractRoutingDataSource
类里找,可以找到afterPropertiesSet()
方法,这个方法是InitializingBean
接口的,作用是在bean的所有属性设置完成后便会调用此方法。可以看到this.resolvedDataSources
是从this.targetDataSources
取的信息。
所以只需要改变this.targetDataSources,即可改变this.resolvedDataSources;后续改变determineCurrentLookupKey()的返回值(key),在调用getConnection()时即可获取到指定的数据源
实现方式:注解+切面
别看步骤挺多,但其实很容易理解和使用
1.配置文件示例:
spring:
datasource:
master: # 数据源1
jdbc-url: jdbc:mysql://localhost:3306/master?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db1: # 数据源1
jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
2.创建数据源配置类
创建数据源配置类(我这里为了方便区分就为每一个数据源创建了一个配置类,当然也可以把所有的数据源配置在一个类里)
@Configuration
@EnableConfigurationProperties({MasterDataSourceProperties.class})
public class MasterDataSourceConfig {
/**
* 这个MasterDataSourceProperties是读取配置文件的类,我这里为了省篇幅就不展示了
**/
@Autowired
private MasterDataSourceProperties masterDataSourceProperties;
@Bean
@Primary
public DataSource masterDataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(masterDataSourceProperties.getUrl());
datasource.setUsername(masterDataSourceProperties.getUsername());
datasource.setPassword(AESUtil.aesDecode(masterDataSourceProperties.getPassword()));
datasource.setDriverClassName(masterDataSourceProperties.getDriverClassName());
......
return datasource;
}
}
@Configuration
@EnableConfigurationProperties({OdsDataSourceProperties.class})
public class DB1DataSourceConfig {
/**
* 这个DB1DataSourceProperties是读取配置文件的类,我这里为了省篇幅就不展示了
**/
@Autowired
private DB1DataSourceProperties dB1DataSourceProperties;
@Bean
public DataSource db1DataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(dB1DataSourceProperties.getUrl());
datasource.setUsername(dB1DataSourceProperties.getUsername());
datasource.setPassword(AESUtil.aesDecode(dB1DataSourceProperties.getPassword()));
datasource.setDriverClassName(dB1DataSourceProperties.getDriverClassName());
......
return datasource;
}
}
3.创建DynamicDataSource
创建自己的一个DynamicDataSource
类(名字任意)继承AbstractRoutingDataSource
,维护数据源,提供切换方法。
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
* 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
*/
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
/**
* 如果希望所有数据源在启动配置时就加载好,然后通过设置数据源Key值来切换数据,定制这个方法
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
/**
* 设置默认数据源
*
* @param defaultDataSource
*/
public void setDefaultDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
/**
* 设置数据源
*
* @param dataSources
*/
public void setDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
// 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
}
}
4.创建数据源上下文处理器DynamicDataSourceContextHolder
创建数据源上下文处理器DynamicDataSourceContextHolder
用以存储当前线程需要使用的数据源名称。
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 将 master 数据源的 key作为默认数据源的 key
*/
@Override
protected String initialValue() {
return "master";
}
};
/**
* 数据源的 key集合,用于切换时判断数据源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* 切换数据源
*
* @param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* 获取数据源
*
* @return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 重置数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
/**
* 判断是否包含数据源
*
* @param key 数据源key
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* 添加数据源keys
*
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
5.创建数据源配置类DataSourceConfig
创建数据源配置类DataSourceConfig
,将所有数据源注入到spring容器
@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) // 排除 DataSourceAutoConfiguration 的自动配置,避免环形调用
public class DataSourceConfig {
@Autowired
private MasterDataSourceConfig masterDataSourceConfig;
@Autowired
private DB1DataSourceConfig dB1DataSourceConfig;
/**
* 设置动态数据源为主数据源
*
* @return
*/
@Bean
@Primary
public DynamicDataSource dataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认指定的数据源
dynamicDataSource.setDefaultDataSource(masterDataSourceConfig.masterDataSource());
// 将数据源设置进map
Map<Object, Object> dataSourceMap = new HashMap<>(8);
dataSourceMap.put(DataSourceEnum.MASTER.toString(), masterDataSourceConfig.masterDataSource());
dataSourceMap.put(DataSourceEnum.DB1.toString(), dB1DataSourceConfig.db1DataSource());
// 使用 Map 保存多个数据源,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上
dynamicDataSource.setDataSources(dataSourceMap);
return dynamicDataSource;
}
}
6.创建数据源类型枚举DataSourceEnum
public enum DataSourceEnum {
/**默认类型*/
MASTER,
/**DB1类型*/
DB1,
;
}
7.创建自定义注解@DataSource
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 数据源key值
* @return
*/
DataSourceEnum value();
}
8.创建切面DynamicDataSourceAspect
@Slf4j
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {
/**
* 切换数据源
*
* @param point
* @param dataSource
*/
@Before("@annotation(dataSource))")
public void switchDataSource(JoinPoint point, DataSource dataSource) {
if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value().toString())) {
log.info("DataSource [{}] doesn't exist, use default DataSource", dataSource.value());
} else {
// 切换数据源
DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().toString());
log.info("Switch DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(),
point.getSignature());
}
}
/**
* 重置数据源
*
* @param point
* @param dataSource
*/
@After("@annotation(dataSource))")
public void restoreDataSource(JoinPoint point, DataSource dataSource) {
// 将数据源置为默认数据源
DynamicDataSourceContextHolder.clearDataSourceKey();
log.info("Restore DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
}
}
如何使用
@Override
@DataSource(DataSourceEnum.DB1)
public Page<AuditTaskDto> queryAuditTask(AuditTaskQuery query) {
Page<AuditTaskDto> page = baseMapper.queryAuditTask(query);
return page;
}