??最近有一個(gè)項(xiàng)目國(guó)際化的需求,解決方案一般是這樣的:WEB網(wǎng)站國(guó)際化的一種解決方案。
??簡(jiǎn)單來(lái)說(shuō),國(guó)際化一方面需要配置靜態(tài)文字,另一方面需要管理動(dòng)態(tài)數(shù)據(jù)。靜態(tài)文字國(guó)際化可參考:SpringBoot項(xiàng)目國(guó)際化;;SpringBoot的國(guó)際化錯(cuò)誤信息返回,下文我們主要講的就是動(dòng)態(tài)數(shù)據(jù)國(guó)際化。
??實(shí)現(xiàn)思路:利用AOP或攔截器實(shí)現(xiàn)數(shù)據(jù)庫(kù)動(dòng)態(tài)切換。
動(dòng)態(tài)數(shù)據(jù)源切換時(shí)會(huì)遇到事務(wù)的問(wèn)題,這個(gè)問(wèn)題暫時(shí)還未考慮,下文也不涉及,這個(gè)坑留著以后再填。。(主要是太懶了)
一、準(zhǔn)備工作
- 創(chuàng)建多個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)名分別為dev,dev_hk,dev_en,每個(gè)數(shù)據(jù)庫(kù)的表名是一樣的。
- 添加依賴
pom.xml,下面將利用AOP實(shí)現(xiàn)數(shù)據(jù)源動(dòng)態(tài)切換,所以要引入aop的依賴。
<!-- 引入aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
-
application.yml配置文件中配置數(shù)據(jù)源;
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
# 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置監(jiān)控統(tǒng)計(jì)攔截的filters,去掉后監(jiān)控界面sql無(wú)法統(tǒng)計(jì),'wall'用于防火墻
filters: stat,wall,log4j
# 通過(guò)connectProperties屬性來(lái)打開(kāi)mergeSql功能;慢SQL記錄
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 自定義屬性,用于druid監(jiān)控界面的賬號(hào)、密碼配置
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 # 邏輯已刪除值(默認(rèn)為 0)
logic-not-delete-value: 1 # 邏輯未刪除值(默認(rèn)為 1)
二、準(zhǔn)備工作
- 數(shù)據(jù)源配置類,獲取取application中數(shù)據(jù)源的配置,分別構(gòu)建三個(gè)數(shù)據(jù)源。
public class DynamicDataSourceConfig {
/**
* 簡(jiǎn)體中文數(shù)據(jù)庫(kù) application.yml spring.datasource.cn 配置信息
*
* @return DataSource
*/
@Bean(name = "cnDataSource")
@ConfigurationProperties("spring.datasource.cn")
public DataSource cnDataSource() {
return new DruidDataSource();
}
/**
* 繁體中文數(shù)據(jù)庫(kù) application.yml spring.datasource.hk 配置信息
*
* @return DataSource
*/
@Bean(name = "hkDataSource")
@ConfigurationProperties("spring.datasource.hk")
public DataSource hkDataSource() {
return new DruidDataSource();
}
/**
* 英文數(shù)據(jù)庫(kù) application.yml spring.datasource.en 配置信息
*
* @return DataSource
*/
@Bean(name = "enDataSource")
@ConfigurationProperties("spring.datasource.en")
public DataSource enDataSource() {
return new DruidDataSource();
}
/**
* 我們自定義的數(shù)據(jù)源DynamicRoutingDataSource添加到Spring容器里面去
*
* @param cnDataSource 簡(jiǎn)體中文數(shù)據(jù)庫(kù)
* @param hkDataSource 繁體中文數(shù)據(jù)庫(kù)
* @param enDataSource 英文數(shù)據(jù)庫(kù)
*/
@Bean
@Primary
public DynamicRoutingDataSource dataSource(DataSource cnDataSource, DataSource hkDataSource, DataSource enDataSource) {
Map<Object, Object> targetDataSources = Maps.newHashMapWithExpectedSize(3);
// 每個(gè)key對(duì)應(yīng)一個(gè)數(shù)據(jù)源
targetDataSources.put(DataSourceType.CNZH, cnDataSource);
targetDataSources.put(DataSourceType.HKZH, hkDataSource);
targetDataSources.put(DataSourceType.USEN, enDataSource);
return new DynamicRoutingDataSource(cnDataSource, targetDataSources);
}
}
- 配置數(shù)據(jù)源上下文以及動(dòng)態(tài)數(shù)據(jù)源路由。
首先要新建一個(gè)數(shù)據(jù)源上下文,通過(guò) ThreadLocal 獲取和設(shè)置線程安全的數(shù)據(jù)源 key,記錄當(dāng)前線程使用的數(shù)據(jù)源的key是什么,以及記錄所有注冊(cè)成功的數(shù)據(jù)源的key的集合。那么怎么通知spring用key當(dāng)前的數(shù)據(jù)源呢,spring提供一個(gè)名為AbstractRoutingDataSource的抽象類,我們只需要重寫(xiě)determineCurrentLookupKey方法就可以,這個(gè)方法返回當(dāng)前線程的數(shù)據(jù)源的key,我們只需要從我們剛剛的數(shù)據(jù)源上下文中取出我們的key即可,具體代碼如下:
/**
* @Description: 動(dòng)態(tài)數(shù)據(jù)源設(shè)置,每次訪問(wèn)之前設(shè)置,訪問(wèn)完成之后在清空
* (AbstractRoutingDataSource相當(dāng)于數(shù)據(jù)源路由中介,能有在運(yùn)行時(shí), 根據(jù)某種key值來(lái)動(dòng)態(tài)切換到真正的DataSource上)
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 使用ThreadLocal維護(hù)變量,ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,
* 所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。
*/
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
public static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
/**
* 構(gòu)造函數(shù)
*
* @param defaultTargetDataSource 默認(rèn)的數(shù)據(jù)源
* @param targetDataSources 多數(shù)據(jù)源每個(gè)key對(duì)應(yīng)一個(gè)數(shù)據(jù)源
*/
public DynamicRoutingDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
// 設(shè)置默認(rèn)數(shù)據(jù)源
super.setDefaultTargetDataSource(defaultTargetDataSource);
// 設(shè)置多數(shù)據(jù)源. key value的形式
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
/**
* 多數(shù)據(jù)源對(duì)應(yīng)的key, 會(huì)通過(guò)這個(gè)key找到我們需要的數(shù)據(jù)源
*/
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
/**
* 設(shè)置使用哪個(gè)數(shù)據(jù)源
*
* @param dataSource 數(shù)據(jù)源對(duì)應(yīng)的名字
*/
public static void setDataSource(DataSourceType dataSource) {
log.info("切換到{}數(shù)據(jù)源", dataSource);
contextHolder.set(dataSource);
}
/**
* 獲取數(shù)據(jù)源對(duì)應(yīng)的名字
*
* @return 數(shù)據(jù)源對(duì)應(yīng)的名字
*/
public static DataSourceType getDataSource() {
return contextHolder.get();
}
/**
* 清空掉
*/
public static void clearDataSource() {
contextHolder.remove();
}
}
- 數(shù)據(jù)源類型枚舉類
public enum DataSourceType {
/**
* 中文簡(jiǎn)體
*/
CNZH,
/**
* 中文繁體
*/
HKZH,
/**
* 美國(guó)英文
*/
USEN
}
- 自定義注解。
現(xiàn)在spring也已經(jīng)知道通過(guò)key來(lái)取對(duì)應(yīng)的數(shù)據(jù)源,我們需要在需要切換數(shù)據(jù)源的方法上設(shè)置數(shù)據(jù)源的key,并且保存在數(shù)據(jù)源上下文中。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceAnnotation {
/**
* 數(shù)據(jù)源類型
* @return 數(shù)據(jù)源類型
*/
DataSourceType sourceType();
}
- 切點(diǎn)可以是DataSourceAnnotation注解,所有添加了
@DataSurceAnnotation的方法都進(jìn)入切面,并根據(jù)傳入的參數(shù)進(jìn)行相應(yīng)的切換。
@Component
@Aspect
@Order(value = 1) //這是關(guān)鍵,要讓該切面調(diào)用先于AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {
/**
* 所有添加了DataSurceAnnotation的方法都進(jìn)入切面
*/
@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();
//在執(zhí)行方法之前設(shè)置使用哪個(gè)數(shù)據(jù)源
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();
}
}
}
- 可以測(cè)試一下上述方法,在測(cè)試方法上加注解,通過(guò)參數(shù)
DataSourceType.CNZH切換到中文數(shù)據(jù)源。
@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);
}
- 如果需求是數(shù)據(jù)庫(kù)的讀寫(xiě)分離,通過(guò)上述方法能很好的實(shí)現(xiàn)。但現(xiàn)在的需求是動(dòng)態(tài)切換中英文數(shù)據(jù)庫(kù),所以我改進(jìn)了一下。
改進(jìn)思路:不再通過(guò)添加注解的方式進(jìn)入切面,而是在進(jìn)入controller方法之前,通過(guò)請(qǐng)求頭的Accept-Language參數(shù)動(dòng)態(tài)切換數(shù)據(jù)源’。
我們不再需要自定義注解了,主需要更改切點(diǎn)和切換數(shù)據(jù)源的條件。下面是更改后的:
@Component
@Aspect
@Order(value = 1) //這是關(guān)鍵,要讓該切面調(diào)用先于AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {
/**
* 在所有controller接口前執(zhí)行
*/
@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();
}
}
-
測(cè)試
測(cè)試
