前言
多数据源的核心就是向 IOC 容器注入 AbstractRoutingDataSource 和如何切换数据源。注入的方式可以是注册 BeanDefinition 或者是构建好的 Bean,切换数据源的方式可以是方法参数或者是注解切换(其他的没想象出来),具体由需求决定。
我的需求是统计多个库的数据,将结果写入另一个数据库,统计的数据库数量是不定的,无法通过 @Bean 直接注入,又是统计任务,DAO 层注解切换无法满足,因此选择注册(AbstractRoutingDataSource 的)BeanDefinition 和方法参数切换来实现。下面以统计统计中日韩用户到结果库为例。
配置文件
master 为结果库,其他为被统计的数据库(china、japan 可以用枚举唯一标识,当然也可以用 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
对应的配置类:
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 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。
org.springframework.boot
spring-boot-starter-data-jpa
org.projectlombok
lombok
1.18.22
provided
构建 AbstractRoutingDataSource
Spring 的动态数据源需要注入 AbstractRoutingDataSource,因为配置文件中被统计数据源不是固定的,所以不能通过 @Bean 注解注入,需要手动构建。
要在启动类加上 @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 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 执行的很早,此时 @ConfigurationProperties 的配置参数类还没有注入,因此要手动获取(加 @ConfigurationProperties 注解是为了使 IOC 容器中其他 Bean 能获取配置的 Country,以此来切换数据源)。
下面是 AbstractRoutingDataSource 的实现类 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
数据源切换
数据源的切换由 DataSourceContextHolder 和切面 DynamicDataSourceAspect 控制:
package com.statistics.dynamicds.core;
public class DataSourceContextHolder {
private static final ThreadLocal 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 {
Country country = null;
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof Country) {
country = (Country) arg;
}
}
if (country != null) {
DataSourceContextHolder.setLookupKey(country);
}
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
总结
以上就完成了多数据源配置,使用时只需要按照在 dao 层的方法参数中加一个 Country 枚举就可以了。
如果无法用枚举标识数据源也可以换成 String,关于这个数据源的其他信息在内部类 DataSourceProperties 加一个 map 即可,总之就是按照自己的需求扩展。