SpringBoot多數(shù)據(jù)源配置的全過程記錄
前言
多數(shù)據(jù)源的核心就是向 IOC 容器注入 AbstractRoutingDataSource 和如何切換數(shù)據(jù)源。注入的方式可以是注冊(cè) BeanDefinition 或者是構(gòu)建好的 Bean,切換數(shù)據(jù)源的方式可以是方法參數(shù)或者是注解切換(其他的沒想象出來),具體由需求決定。
我的需求是統(tǒng)計(jì)多個(gè)庫的數(shù)據(jù),將結(jié)果寫入另一個(gè)數(shù)據(jù)庫,統(tǒng)計(jì)的數(shù)據(jù)庫數(shù)量是不定的,無法通過 @Bean 直接注入,又是統(tǒng)計(jì)任務(wù),DAO 層注解切換無法滿足,因此選擇注冊(cè)(AbstractRoutingDataSource 的)BeanDefinition 和方法參數(shù)切換來實(shí)現(xiàn)。下面以統(tǒng)計(jì)統(tǒng)計(jì)中日韓用戶到結(jié)果庫為例。
配置文件
master 為結(jié)果庫,其他為被統(tǒng)計(jì)的數(shù)據(jù)庫(china、japan 可以用枚舉唯一標(biāo)識(shí),當(dāng)然也可以用 String):
dynamic:
dataSources:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/result?useUnicode=true&characterEncoding=utf8xxxxxxxx
username: root
password: 123456
china:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/china?useUnicode=true&characterEncoding=utf8xxxxxxxx
username: root
password: 123456
japan:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/japan?useUnicode=true&characterEncoding=utf8xxxxxxxx
username: root
password: 123456
korea:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/korea?useUnicode=true&characterEncoding=utf8xxxxxxxx
username: root
password: 123456
對(duì)應(yīng)的配置類:
package com.statistics.dynamicds.core.config;
import com.statistics.dynamicds.core.Country;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import static com.statistics.dynamicds.core.config.MultiDataSourceProperties.PREFIX;
@Data
@Configuration
@ConfigurationProperties(prefix = PREFIX)
public class MultiDataSourceProperties {
public static final String PREFIX = "dynamic";
private Map<Country, DataSourceProperties> dataSources;
@Data
public static class DataSourceProperties {
private String driverClassName;
private String url;
private String username;
private String password;
}
}
package com.statistics.dynamicds.core;
public enum Country {
MASTER("master", 0),
CHINA("china", 86),
JAPAN("japan", 81),
KOREA("korea", 82),
// 其他國家省略
private final String name;
private final int id;
Country(String name, int id) {
this.name = name;
this.id = id;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
依賴
ORM 用的 JPA,SpringBoot 版本為 2.3.7.RELEASE,通過 Lombok 簡(jiǎn)化 GetSet。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
構(gòu)建 AbstractRoutingDataSource
Spring 的動(dòng)態(tài)數(shù)據(jù)源需要注入 AbstractRoutingDataSource,因?yàn)榕渲梦募斜唤y(tǒng)計(jì)數(shù)據(jù)源不是固定的,所以不能通過 @Bean 注解注入,需要手動(dòng)構(gòu)建。
要在啟動(dòng)類加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class)。
要在啟動(dòng)類加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class)。
要在啟動(dòng)類加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class),重要的事情寫三行。
package com.statistics.dynamicds.autoconfig;
import com.statistics.dynamicds.core.DynamicDataSourceRouter;
import com.statistics.dynamicds.core.Country;
import com.statistics.dynamicds.core.config.MultiDataSourceProperties;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import javax.annotation.Nonnull;
import java.util.Map;
import java.util.stream.Collectors;
import static com.statistics.dynamicds.core.config.MultiDataSourceProperties.PREFIX;
public class MultiDataSourceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
public static final String DATASOURCE_BEANNAME = "dynamicDataSourceRouter";
private Environment environment;
@Override
public void registerBeanDefinitions(@Nonnull AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
MultiDataSourceProperties multiDataSourceProperties = Binder.get(environment)
.bind(PREFIX, MultiDataSourceProperties.class)
.orElseThrow(() -> new RuntimeException("no found dynamicds config"));
final HikariDataSource[] defaultTargetDataSource = {null};
Map<Country, HikariDataSource> targetDataSources = multiDataSourceProperties.getDataSources().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> {
MultiDataSourceProperties.DataSourceProperties dataSourceProperties = entry.getValue();
HikariDataSource dataSource = DataSourceBuilder.create()
.type(HikariDataSource.class)
.driverClassName(dataSourceProperties.getDriverClassName())
.url(dataSourceProperties.getUrl())
.username(dataSourceProperties.getUsername())
.password(dataSourceProperties.getPassword())
.build();
dataSource.setPoolName("HikariPool-" + entry.getKey());
if (Country.MASTER == entry.getKey()) {
defaultTargetDataSource[0] = dataSource;
}
return dataSource;
}));
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(DynamicDataSourceRouter.class)
.addConstructorArgValue(defaultTargetDataSource[0])
.addConstructorArgValue(targetDataSources)
.getBeanDefinition();
registry.registerBeanDefinition(DATASOURCE_BEANNAME, beanDefinition);
}
@Override
public void setEnvironment(@Nonnull Environment environment) {
this.environment = environment;
}
}
上面代碼中 MultiDataSourceProperties 不是由 @Resource 或者 @Autowired 獲取的是因?yàn)?ImportBeanDefinitionRegistrar 執(zhí)行的很早,此時(shí) @ConfigurationProperties 的配置參數(shù)類還沒有注入,因此要手動(dòng)獲取(加 @ConfigurationProperties 注解是為了使 IOC 容器中其他 Bean 能獲取配置的 Country,以此來切換數(shù)據(jù)源)。
下面是 AbstractRoutingDataSource 的實(shí)現(xiàn)類 DynamicDataSourceRouter:
package com.statistics.dynamicds.core;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;
public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
public DynamicDataSourceRouter(Object defaultTargetDataSource, Map<Object, Object> targetDataSources) {
this.setDefaultTargetDataSource(defaultTargetDataSource);
this.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getLookupKey();
}
}
數(shù)據(jù)源切換
數(shù)據(jù)源的切換由 DataSourceContextHolder 和切面 DynamicDataSourceAspect 控制:
package com.statistics.dynamicds.core;
public class DataSourceContextHolder {
private static final ThreadLocal<Country> HOLDER = ThreadLocal.withInitial(() -> Country.MASTER);
public static void setLookupKey(Country lookUpKey) {
HOLDER.set(lookUpKey);
}
public static Country getLookupKey() {
return HOLDER.get();
}
public static void clear() {
HOLDER.remove();
}
}
package com.statistics.dynamicds.core;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DynamicDataSourceAspect {
@Pointcut("execution(* com.statistics.dao..*.*(..))")
void aspect() {
}
@Around("aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof Country) {
DataSourceContextHolder.setLookupKey((Country) arg);
break;
}
}
try {
return joinPoint.proceed();
}finally {
DataSourceContextHolder.clear();
}
}
}
目錄
.
└─com
└─statistics
│ StatisticsApplication.java
│
├─dao
│ UserDao.java
│
├─dynamicds
│ ├─autoconfig
│ │ MultiDataSourceImportBeanDefinitionRegistrar.java
│ │
│ └─core
│ │ DataSourceContextHolder.java
│ │ DynamicDataSourceAspect.java
│ │ DynamicDataSourceRouter.java
│ │ Province.java
│ │
│ └─config
│ MultiDataSourceProperties.java
總結(jié)
以上就完成了多數(shù)據(jù)源配置,使用時(shí)只需要按照在 dao 層的方法參數(shù)中加一個(gè) Country 枚舉就可以了。
如果無法用枚舉標(biāo)識(shí)數(shù)據(jù)源也可以換成 String,關(guān)于這個(gè)數(shù)據(jù)源的其他信息在內(nèi)部類 DataSourceProperties 加一個(gè) map 即可,總之就是按照自己的需求擴(kuò)展。
相關(guān)文章
淺談基于Token的WEB后臺(tái)認(rèn)證機(jī)制
這篇文章主要介紹了淺談基于Token的WEB后臺(tái)認(rèn)證機(jī)制,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-12-12
SpringData JPA實(shí)體映射與關(guān)系映射的實(shí)現(xiàn)
Spring Data JPA作為Spring生態(tài)系統(tǒng)中的核心項(xiàng)目,通過JPA規(guī)范提供了優(yōu)雅而強(qiáng)大的實(shí)體映射與關(guān)系處理機(jī)制,下面就來介紹一下,感興趣的可以了解一下2025-04-04
JavaFX實(shí)現(xiàn)簡(jiǎn)易時(shí)鐘效果(一)
這篇文章主要為大家詳細(xì)介紹了JavaFX實(shí)現(xiàn)簡(jiǎn)易時(shí)鐘效果的第一篇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
Spring MVC溫故而知新系列教程之請(qǐng)求映射RequestMapping注解
這篇文章主要介紹了Spring MVC溫故而知新系列教程之請(qǐng)求映射RequestMapping注解的相關(guān)知識(shí),文中給大家介紹了RequestMapping注解提供的幾個(gè)屬性及注解說明,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05
Fluent Mybatis如何做到代碼邏輯和sql邏輯的合一
對(duì)比原生Mybatis, Mybatis Plus或者其他框架,F(xiàn)luentMybatis提供了哪些便利呢?很多朋友對(duì)這一問題不是很清楚,今天小編給大家?guī)硪黄坛剃P(guān)于Fluent Mybatis如何做到代碼邏輯和sql邏輯的合一,一起看看吧2021-08-08
Spring Boot 2.0快速構(gòu)建服務(wù)組件全步驟
這篇文章主要給大家介紹了關(guān)于Spring Boot 2.0快速構(gòu)建服務(wù)組件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot 2.0具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

