SpringBoot多數(shù)據(jù)源配置的全過程記錄
前言
多數(shù)據(jù)源的核心就是向 IOC 容器注入 AbstractRoutingDataSource 和如何切換數(shù)據(jù)源。注入的方式可以是注冊 BeanDefinition 或者是構(gòu)建好的 Bean,切換數(shù)據(jù)源的方式可以是方法參數(shù)或者是注解切換(其他的沒想象出來),具體由需求決定。
我的需求是統(tǒng)計多個庫的數(shù)據(jù),將結(jié)果寫入另一個數(shù)據(jù)庫,統(tǒng)計的數(shù)據(jù)庫數(shù)量是不定的,無法通過 @Bean 直接注入,又是統(tǒng)計任務(wù),DAO 層注解切換無法滿足,因此選擇注冊(AbstractRoutingDataSource 的)BeanDefinition 和方法參數(shù)切換來實現(xiàn)。下面以統(tǒng)計統(tǒng)計中日韓用戶到結(jié)果庫為例。
配置文件
master 為結(jié)果庫,其他為被統(tǒng)計的數(shù)據(jù)庫(china、japan 可以用枚舉唯一標(biāo)識,當(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
對應(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 簡化 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 的動態(tài)數(shù)據(jù)源需要注入 AbstractRoutingDataSource,因為配置文件中被統(tǒng)計數(shù)據(jù)源不是固定的,所以不能通過 @Bean 注解注入,需要手動構(gòu)建。
要在啟動類加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class)。
要在啟動類加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class)。
要在啟動類加上 @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 獲取的是因為 ImportBeanDefinitionRegistrar 執(zhí)行的很早,此時 @ConfigurationProperties 的配置參數(shù)類還沒有注入,因此要手動獲?。?@ConfigurationProperties 注解是為了使 IOC 容器中其他 Bean 能獲取配置的 Country,以此來切換數(shù)據(jù)源)。
下面是 AbstractRoutingDataSource 的實現(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ù)源配置,使用時只需要按照在 dao 層的方法參數(shù)中加一個 Country 枚舉就可以了。
如果無法用枚舉標(biāo)識數(shù)據(jù)源也可以換成 String,關(guān)于這個數(shù)據(jù)源的其他信息在內(nèi)部類 DataSourceProperties 加一個 map 即可,總之就是按照自己的需求擴(kuò)展。
相關(guān)文章
Java五種方式實現(xiàn)多線程循環(huán)打印問題
本文主要介紹了Java五種方式實現(xiàn)多線程循環(huán)打印問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12Java多條件判斷場景中規(guī)則執(zhí)行器的設(shè)計
近日在公司領(lǐng)到一個小需求,需要對之前已有的試用用戶申請規(guī)則進(jìn)行拓展。本文去掉if 判斷,試試用一個規(guī)則執(zhí)行器來替代它,感興趣的可以了解一下2021-06-06Java字符串相關(guān)類StringBuffer的用法詳解
java.lang包下的StringBuffer類,代表著可變的字符序列,可以用來對字符串內(nèi)容進(jìn)行增刪改操作。本文將通過示例詳細(xì)說說它的用法,感興趣的可以跟隨小編一起學(xué)習(xí)一下2022-10-10