本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。
源码上传至 Github: https://github.com/WarsFeng/SpringBoot_MultiDB
1) 前置知识点
1.1) 注解
注解仅为一个标识,起到注释或辅助程序运行的作用。
1.1.1) Meta Annotation
Java 提供了四个标准元注解,用于说明自定义注解的作用域
- @Target: 注解使用范围,Type \ Field \ Method \ Parameter ...
- @Retention: 存在于哪个阶段,Source \ Class \ Runtime
- @Document: 包含进 JavaDoc
- @Inherited: 标识的注解会被子类继承
- 有一例 @A,注解了 Class C,Class C2 extend C,则 C2 也拥有 @A
2) SpringBoot 实现读写分离
一般结合 MyBatis 使用,实现多数据源的支持。
2.1) Config
首先在 application.yml 中定义两个数据源 reader, writer:
spring:
datasource:
reader:
url: jdbc:mysql://rcat:3306/multi_db?useUnicode=true@characterEncoding=utf-8
username: root
password: passwd
driver-class-name: com.mysql.cj.jdbc.Driver
writer:
url: jdbc:mysql://rcat:3306/multi_db2?useUnicode=true@characterEncoding=utf-8
username: root
password: passwd
driver-class-name: com.mysql.cj.jdbc.Driver
2.2) Enum and Annotation
我们将使用注解切换数据源,定义我们的枚举 DataSourceType
和注解 DBSelector
, 注解为 Method
级别注解,实现函数内的数据源选择。
public enum DataSourceType {
READER,
WRITER
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DBSelector {
DataSourceType value() default DataSourceType.READER;
}
2.3) ThreadLocal handler
使用 ThreadLocal
保证代码优雅,定义数据源绑定器,下文会利用 AOP 将 dbType
set 进 ThreadLocal
。
public class DataSourceBinding {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void bindDataSource(DataSourceType dbType) {
threadLocal.set(dbType.getValue());
}
public static String getDataSource() {
return threadLocal.get();
}
public static void clearDataSource() {
threadLocal.remove();
}
}
2.4) Cut Aspect
本文仅实现 Method
级别的数据源切换,为 @DBSelector
标注域内绑定上 dbType
, 方法开始时 bind,结束后 clear
@Aspect
@Order(1)
@Component
public class MultiDataSourceAspect {
@Around("@annotation(cat.wars.course.multidb.annotation.DBSelector)")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
DBSelector dbSelector = signature.getMethod().getAnnotation(DBSelector.class);
if (null != dbSelector) {
DataSourceBinding.bindDataSource(dbSelector.value().name());
}
try {
return point.proceed();
} finally {
DataSourceBinding.clearDataSource();
}
}
}
2.5) Configuration
这是最重要的一个部分,Spring-JDBC 内置了多数据源的支持,基于 DataSource 实现了 AbstractRoutingDataSource
接口。
原理:AbstractRoutingDataSource
内置了一个 Map 可用于存储我们的多个数据源, getConnection
时会从 Map 中尝试 Get
, 用 determineCurrentLookupKey
返回值当作 Key, 所以我们应该怎么实现这个抽象类呢:
- 构造函数中将多个数据源 Put 进 Map,使用
DataSourceType
当 Key determineCurrentLookupKey
中直接将ThreadLocal
中的dbType
取出即完成~
接着用 @Bean
和 @Primary
直接塞进容器中,这里我直接用内部类写在 Configuratoin 中了 =。=
@Configuration
public class MultiDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.reader")
public DataSource reader() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.writer")
public DataSource writer() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dataSource(DataSource reader, DataSource writer) {
HashMap<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.READER.name(), reader);
targetDataSources.put(DataSourceType.WRITER.name(), writer);
return new DynamicDataSource(reader, targetDataSources);
}
/** 动态数据源 DataSource */
private static final class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
this.setDefaultTargetDataSource(defaultDataSource);
if (null == targetDataSources) return;
this.setTargetDataSources(targetDataSources);
this.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceBinding.getDataSource();
}
}
}
接着由于 SpringBoot 的默认配置会在启动时使用 DataSourceAutoConfiguration
获取配置中的 spring.datasource
初始化默认的 DataSource
, 我们需要在 SpringBoot 的启动器中排除掉它,如:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MultiDbApplication {
public static void main(String[] args) {
SpringApplication.run(MultiDbApplication.class, args);
}
}
2.6) Test
测试!这里用 SpringMVC 测试效果:
@RestController
public class ReadWriteController {
private final JdbcTemplate jdbcTemplate;
public ReadWriteController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@GetMapping("/read")
@DBSelector(DataSourceType.READER)
public String read() {
return jdbcTemplate.queryForList("SELECT * FROM user").toString();
}
@GetMapping("/write")
@DBSelector(DataSourceType.WRITER)
public String write() {
return jdbcTemplate.queryForList("SELECT * FROM t_user").toString();
}
}