1. 数据源准备
1.1 创建配置文件 application.yaml
spring:
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
jdbc-url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&allowMultiQueries=true
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
jdbc-url: jdbc:mysql://localhost:3306/slave?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&allowMultiQueries=true
1.2 数据库数据
存在两个库 master、slave,每个库中都有一个 route 表,每个表中有一条数据,其中 sign 为库名
2. 相关代码
2.1 创建实体类 DynamicDataSourceContextHolder
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void set(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static String get() {
return CONTEXT_HOLDER.get();
}
public static void remove() {
CONTEXT_HOLDER.remove();
}
}
用于获取、设置、移除数据源
2.2 创建实体类 DynamicDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.get();
}
}
DynamicDataSource 继承自 AbstractRoutingDataSource,AbstractRoutingDataSource 的 getConnection 方法会通过调用 determineTargetDataSource 方法推断数据源
2.3 创建枚举类 DataSourceEnum
public enum DataSourceEnum {
MASTER("master"),
SLAVE("slave");
private final String value;
public String getValue() {
return value;
}
DataSourceEnum(String value) {
this.value = value;
}
}
2.4 创建配置类 DataSourceConfig
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@Primary
public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceEnum.MASTER.getValue(), masterDataSource);
dataSourceMap.put(DataSourceEnum.SLAVE.getValue(), slaveDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
2.4.1 注意点
- masterDataSource()、slaveDataSource() 方法的返回值的类型是 HikariDataSource,HikariDataSource 只存在属性 jdbcUrl,不存在属性 url,所以配置文件 application.yaml 的相关属性要改成 jdbcUrl
- dynamicDataSource() 方法需要添加 @Primary 注解,因为当前环境中存在3个类型为 DataSource 的 bean,这样可以让其他依赖 DataSource 的 bean 优先选择 DynamicDataSource,然后再通过 DynamicDataSource 的 determineCurrentLookupKey 方法决定实际使用的数据源
3. 自定义注解用于切换数据源
3.1 创建自定义注解 DataSourceRoute
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceRoute {
DataSourceEnum value() default DataSourceEnum.MASTER;
}
3.2 创建切面 DataSourceRouteAspect
@Component
@Aspect
public class DataSourceRouteAspect {
@Pointcut("@annotation(com.ys.blog.annotation.DataSourceRoute)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature ms = (MethodSignature) pjp.getSignature();
Method method = pjp.getTarget().getClass().getMethod(ms.getName(), ms.getParameterTypes());
if (method.isAnnotationPresent(DataSourceRoute.class)) {
DataSourceRoute dataSourceRoute = method.getAnnotation(DataSourceRoute.class);
DataSourceEnum dataSourceEnum = dataSourceRoute.value();
DynamicDataSourceContextHolder.set(dataSourceEnum.getValue());
}
return pjp.proceed(pjp.getArgs());
}
}
4. 引入 mybatis
4.1 创建 RouteMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ys.blog.mapper.RouteMapper">
<select id="getSign" resultType="java.lang.String">
select sign from route where id = 1
</select>
</mapper>
4.2 创建接口 RouteMapper
public interface RouteMapper {
String getSign();
}
5. 其他
5.1 创建 RouteController
@RestController
@RequestMapping("/route")
public class RouteController {
@Autowired
private RouteMapper routeMapper;
@GetMapping("/master")
@DataSourceRoute(DataSourceEnum.MASTER)
public String masterRoute() {
return routeMapper.getSign();
}
@GetMapping("/slave")
@DataSourceRoute(DataSourceEnum.SLAVE)
public String slaveRoute() {
return routeMapper.getSign();
}
}
5.2 创建启动类 BlogApplication
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableConfigurationProperties
@MapperScan(value = "com.ys.blog.mapper")
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
5.2.1 注意点
注解 @SpringBootApplication 需要移除自动配置类 DataSourceAutoConfiguration,不然会导致循环依赖
5.2.2 原因简述
DataSourceAutoConfiguration 会导入一个类型为 DataSourceInitializerPostProcessor 的 BeanPostProcessor。如果 bean 的类型是 DataSource,会实例化一个类型为 DataSourceInitializerInvoker 的 bean,DataSourceInitializerInvoker 的构造方法依赖 DataSource
综上所述:DynamicDataSource 的实例化会触发 masterDataSource() 方法执行,masterDataSource() 方法的返回值类型是 DataSource,所以又会触发 DataSourceInitializerInvoker 的实例化,DataSourceInitializerInvoker 的实例化又依赖一个类型为 DataSource 的 bean,所以就产生了循环依赖
6. 测试
6.1 访问 master 数据源
通过运行结果,得出结论:访问的是 master 数据源
6.2 访问 slave 数据源
通过运行结果,得出结论:访问的是 slave 数据源