SpringBoot(三)动态数据源切换
2020-02-05 本文已影响0人
TiaNa_na
最近有一个项目国际化的需求,解决方案一般是这样的:WEB网站国际化的一种解决方案。
简单来说,国际化一方面需要配置静态文字,另一方面需要管理动态数据。静态文字国际化可参考:SpringBoot项目国际化;;SpringBoot的国际化错误信息返回,下文我们主要讲的就是动态数据国际化。
实现思路:利用AOP或拦截器实现数据库动态切换。
动态数据源切换时会遇到事务的问题,这个问题暂时还未考虑,下文也不涉及,这个坑留着以后再填。。(主要是太懒了)
一、准备工作
- 创建多个数据库,数据库名分别为dev,dev_hk,dev_en,每个数据库的表名是一样的。
- 添加依赖
pom.xml
,下面将利用AOP实现数据源动态切换,所以要引入aop的依赖。
<!-- 引入aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
-
application.yml
配置文件中配置数据源;
server:
port: 8081
spring:
messages:
basename: i18n/messages
encoding: UTF-8
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
cn:
url: jdbc:mysql://localhost:3306/dev?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
username: test
password: 123456
hk:
url: jdbc:mysql://localhost:3306/dev_hk?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
username: test
password: 123456
en:
url: jdbc:mysql://localhost:3306/dev_en?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
username: test
password: 123456
# 配置连接池
type: com.alibaba.druid.pool.DruidDataSource
druid:
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 自定义属性,用于druid监控界面的账号、密码配置
servlet:
username: test
password: 123456
http:
log-request-details: true
servlet:
multipart:
max-file-size: 20MB
max-request-size: 30MB
file-size-threshold: 0
enabled: true
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
config-location: classpath:mybatis-config.xml
mybatis-plus:
global-config:
db-config:
logic-delete-value: 0 # 逻辑已删除值(默认为 0)
logic-not-delete-value: 1 # 逻辑未删除值(默认为 1)
二、准备工作
- 数据源配置类,获取取application中数据源的配置,分别构建三个数据源。
public class DynamicDataSourceConfig {
/**
* 简体中文数据库 application.yml spring.datasource.cn 配置信息
*
* @return DataSource
*/
@Bean(name = "cnDataSource")
@ConfigurationProperties("spring.datasource.cn")
public DataSource cnDataSource() {
return new DruidDataSource();
}
/**
* 繁体中文数据库 application.yml spring.datasource.hk 配置信息
*
* @return DataSource
*/
@Bean(name = "hkDataSource")
@ConfigurationProperties("spring.datasource.hk")
public DataSource hkDataSource() {
return new DruidDataSource();
}
/**
* 英文数据库 application.yml spring.datasource.en 配置信息
*
* @return DataSource
*/
@Bean(name = "enDataSource")
@ConfigurationProperties("spring.datasource.en")
public DataSource enDataSource() {
return new DruidDataSource();
}
/**
* 我们自定义的数据源DynamicRoutingDataSource添加到Spring容器里面去
*
* @param cnDataSource 简体中文数据库
* @param hkDataSource 繁体中文数据库
* @param enDataSource 英文数据库
*/
@Bean
@Primary
public DynamicRoutingDataSource dataSource(DataSource cnDataSource, DataSource hkDataSource, DataSource enDataSource) {
Map<Object, Object> targetDataSources = Maps.newHashMapWithExpectedSize(3);
// 每个key对应一个数据源
targetDataSources.put(DataSourceType.CNZH, cnDataSource);
targetDataSources.put(DataSourceType.HKZH, hkDataSource);
targetDataSources.put(DataSourceType.USEN, enDataSource);
return new DynamicRoutingDataSource(cnDataSource, targetDataSources);
}
}
- 配置数据源上下文以及动态数据源路由。
首先要新建一个数据源上下文,通过 ThreadLocal 获取和设置线程安全的数据源 key,记录当前线程使用的数据源的key是什么,以及记录所有注册成功的数据源的key的集合。那么怎么通知spring用key当前的数据源呢,spring提供一个名为AbstractRoutingDataSource的抽象类,我们只需要重写determineCurrentLookupKey方法就可以,这个方法返回当前线程的数据源的key,我们只需要从我们刚刚的数据源上下文中取出我们的key即可,具体代码如下:
/**
* @Description: 动态数据源设置,每次访问之前设置,访问完成之后在清空
* (AbstractRoutingDataSource相当于数据源路由中介,能有在运行时, 根据某种key值来动态切换到真正的DataSource上)
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
public static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
/**
* 构造函数
*
* @param defaultTargetDataSource 默认的数据源
* @param targetDataSources 多数据源每个key对应一个数据源
*/
public DynamicRoutingDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
// 设置默认数据源
super.setDefaultTargetDataSource(defaultTargetDataSource);
// 设置多数据源. key value的形式
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
/**
* 多数据源对应的key, 会通过这个key找到我们需要的数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
/**
* 设置使用哪个数据源
*
* @param dataSource 数据源对应的名字
*/
public static void setDataSource(DataSourceType dataSource) {
log.info("切换到{}数据源", dataSource);
contextHolder.set(dataSource);
}
/**
* 获取数据源对应的名字
*
* @return 数据源对应的名字
*/
public static DataSourceType getDataSource() {
return contextHolder.get();
}
/**
* 清空掉
*/
public static void clearDataSource() {
contextHolder.remove();
}
}
- 数据源类型枚举类
public enum DataSourceType {
/**
* 中文简体
*/
CNZH,
/**
* 中文繁体
*/
HKZH,
/**
* 美国英文
*/
USEN
}
- 自定义注解。
现在spring也已经知道通过key来取对应的数据源,我们需要在需要切换数据源的方法上设置数据源的key,并且保存在数据源上下文中。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceAnnotation {
/**
* 数据源类型
* @return 数据源类型
*/
DataSourceType sourceType();
}
- 切点可以是DataSourceAnnotation注解,所有添加了
@DataSurceAnnotation
的方法都进入切面,并根据传入的参数进行相应的切换。
@Component
@Aspect
@Order(value = 1) //这是关键,要让该切面调用先于AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {
/**
* 所有添加了DataSurceAnnotation的方法都进入切面
*/
@Pointcut("@annotation(com.houtang.csms.mps.multisource.DataSourceAnnotation)")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//在执行方法之前设置使用哪个数据源
DataSourceAnnotation ds = method.getAnnotation(DataSourceAnnotation.class);
if (ds == null) {
DynamicRoutingDataSource.setDataSource(DataSourceType.CNZH);
} else {
DynamicRoutingDataSource.setDataSource(ds.sourceType());
}
try {
return point.proceed();
} finally {
DynamicRoutingDataSource.clearDataSource();
}
}
}
- 可以测试一下上述方法,在测试方法上加注解,通过参数
DataSourceType.CNZH
切换到中文数据源。
@DataSourceAnnotation(sourceType = DataSourceType.CNZH)
@Test
public void saveFaqEn() {
FaqEn faq = new FaqEn();
faq.setDeviceType("Lexmark CX725");
faq.setFaqStatus(1);
faq.setFaqSort(1);
faq.setFaqTitle("Printer failure, unable to operate remotely");
faq.setFaqContent("");
faq.setFaqDate(LocalDateTime.now());
faqEnMapper.insert(faq);
}
- 如果需求是数据库的读写分离,通过上述方法能很好的实现。但现在的需求是动态切换中英文数据库,所以我改进了一下。
改进思路:不再通过添加注解的方式进入切面,而是在进入controller方法之前,通过请求头的Accept-Language
参数动态切换数据源’。
我们不再需要自定义注解了,主需要更改切点和切换数据源的条件。下面是更改后的:
@Component
@Aspect
@Order(value = 1) //这是关键,要让该切面调用先于AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {
/**
* 在所有controller接口前执行
*/
@Pointcut("execution(* com.houtang.csms.mps.controller..*.*(..))")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String language = request.getHeader("Accept-Language");
if (language == null) {
DynamicRoutingDataSource.setDataSource(DataSourceType.CNZH);
} else if (language.equals("HK")) {
DynamicRoutingDataSource.setDataSource(DataSourceType.HKZH);
} else if (language.equals("EN")) {
DynamicRoutingDataSource.setDataSource(DataSourceType.USEN);
} else {
DynamicRoutingDataSource.setDataSource(DataSourceType.CNZH);
}
try {
return point.proceed();
} finally {
DynamicRoutingDataSource.clearDataSource();
}
}
-
测试
测试