自定义动态切换数据源
话不多说,直接上干货
一、项目思路梳理
- 自定义一个注解 @DataSource,将来可以将该注解加在 service 层方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源。
- 对于第一步,如果某个方法上面有 @DataSource 注解,那么就将该方法需要使用的数据源名称存入到 ThreadLocal。
- 自定义切面,在切面中解析 @DataSource 注解,当一个方法或者类上面有 @DataSource 注解的时候,将 @DataSource 注解所标记的数据源存入到 ThreadLocal 中。
- 最后,当 Mapper 执行的时候,需要 DataSource,他会自动去 AbstractRoutingDataSource 类中查找需要的数据源,我们只需要在 AbstractRoutingDataSource 中返回 ThreadLocal 中的值即可。
二、项目结构
三、步骤
1、pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.datasource.zz</groupId>
<artifactId>dynamic_datasource</artifactId>
<version>1.0-SNAPSHOT</version>
<name>dynamic_datasource</name>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、yaml 文件
server:
port: 8081
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
ds:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
slave:
url: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: tienchin
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
3、annotation包下-注解DataSource
自定义一个注解 @DataSource,将来可以将该注解加在 service 层方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源。
package org.datasource.zz.annotation;
import org.datasource.zz.datasource.DataSourceType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 这个注解将来可以加在某个service类或者方法上,
* 通过value 属性注定类或者方法应该使用哪一个数据源
* @author 81230
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface DataSource {
/**
* 如果方法上将来 @Datasource 注解,但是却没有指定数据源的名称,那么默认使用 master 数据源
* @return
*/
String value() default DataSourceType.DEFAULT_DS_NAME;
}
4、aspect包下-自定义切面
自定义切面,在切面中解析 @DataSource 注解,当一个方法或者类上面有 @DataSource 注解的时候,将 @DataSource 注解所标记的数据源存入到 ThreadLocal 中。
4.1、DataSourceAspect
package org.datasource.zz.aspect;
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.aspectj.lang.reflect.MethodSignature;
import org.datasource.zz.annotation.DataSource;
import org.datasource.zz.datasource.DynamicDataSourceContextHolder;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class DataSourceAspect {
/**
* @annotation(org.javaboy.dd.annotation.DataSource) 表示方法上有 @DataSource 注解就将方法拦截下来
* @within(org.javaboy.dd.annotation.DataSource) 表示如果类上面有 @DataSource 注解,就将类中的方法拦截下来
*/
@Pointcut("@annotation(org.datasource.zz.annotation.DataSource) || @within(org.datasource.zz.annotation.DataSource)")
public void pc() {
}
@Around("pc()")
public Object around(ProceedingJoinPoint pjp) {
//获取方法上面的有效注解
DataSource dataSource = getDataSource(pjp);
if (dataSource != null) {
//获取注解中数据源的名称
String value = dataSource.value();
DynamicDataSourceContextHolder.setDataSourceType(value);
}
try {
return pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
return null;
}
private DataSource getDataSource(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
//查找方法上的注解
DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (annotation != null) {
//说明方法上面有 @DataSource 注解
return annotation;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
4.2、GloalDataSourceAspect (全局)
package org.datasource.zz.aspect;
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.datasource.zz.datasource.DataSourceType;
import org.datasource.zz.datasource.DynamicDataSourceContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
/**
* 全局数据源切面
*/
@Aspect
@Component
public class GloalDataSourceAspect {
@Autowired
HttpSession session;
@Pointcut("execution(* org.datasource.zz.service.*.*(..))")
public void pc() {
}
@Around("pc()")
public Object around(ProceedingJoinPoint pjp) {
DynamicDataSourceContextHolder.setDataSourceType((String) session.getAttribute(DataSourceType.DS_SESSION_KEY));
try {
return pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
return null;
}
}
5、datasource包下
5.1、DataSourceType
package org.datasource.zz.datasource;
/**
* @author 81230
*/
public interface DataSourceType {
String DEFAULT_DS_NAME = "master";
String DS_SESSION_KEY = "ds_session_key";
}
5.2、DruidProperties
package org.datasource.zz.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.sql.DataSource;
import java.util.Map;
/**
* @author 81230
*/
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
private String type;
private String driverClassName;
private Map<String,Map<String,String>> ds;
// 初始连接数
private Integer initialSize;
// 最小连接池数量
private Integer minIdle;
// 最大连接池数量
private Integer maxActive;
// 配置获取连接等待超时的时间
private Integer maxWait;
/**
* 一会在外部构造好一个 DruidDataSource 对象,
* 但是这个对象只包含三个核心属性 url、username、password
* 在这个方法中,给这个对象设置公共属性
* @param druidDataSource
* @return
*/
public DataSource datasource(DruidDataSource druidDataSource){
druidDataSource.setInitialSize(initialSize);
druidDataSource.setMinIdle(minIdle);
druidDataSource.setMaxActive(maxActive);
druidDataSource.setMaxWait(maxWait);
return druidDataSource;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public Map<String, Map<String, String>> getDs() {
return ds;
}
public void setDs(Map<String, Map<String, String>> ds) {
this.ds = ds;
}
public Integer getInitialSize() {
return initialSize;
}
public void setInitialSize(Integer initialSize) {
this.initialSize = initialSize;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
public Integer getMaxActive() {
return maxActive;
}
public void setMaxActive(Integer maxActive) {
this.maxActive = maxActive;
}
public Integer getMaxWait() {
return maxWait;
}
public void setMaxWait(Integer maxWait) {
this.maxWait = maxWait;
}
}
5.3、DynamicDataSource
最后,当 Mapper 执行的时候,需要 DataSource,他会自动去 AbstractRoutingDataSource 类中查找需要的数据源,我们只需要在 AbstractRoutingDataSource 中返回 ThreadLocal 中的值即可
package org.datasource.zz.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author 81230
*/
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 这个方法用来返回数据源名称,当系统需要获取数据源的时候,会自动调用该方法获取数据源的名称
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
public DynamicDataSource(LoadDataSource loadDataSource){
// 1设置所有数据源
Map<String, DataSource> allDs = loadDataSource.loadAllDataSource();
super.setTargetDataSources(new HashMap<>(allDs));
//2.设置默认的数据源
// 将来,并不是所有的方法上都有 @DataSource 注解,
// 对于那些没有 @DataSource 注解的方法,该使用哪个数据源?
super.setDefaultTargetDataSource(allDs.get(DataSourceType.DEFAULT_DS_NAME));
// 3
super.afterPropertiesSet();
}
}
5.4、DynamicDataSourceContextHolder类
如果某个方法上面有 @DataSource 注解,那么就将该方法需要使用的数据源名称存入到 ThreadLocal。
package org.datasource.zz.datasource;
/**
* 这个类用来存储当前线程所使用的数据源名称
* @author 81230
*/
public class DynamicDataSourceContextHolder {
private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dsType) {
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
5.5、LoadDataSource
package org.datasource.zz.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author 81230
*/
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
@Autowired
DruidProperties druidProperties;
/**
* 加载全部数据源
* @return
*/
public Map<String, DataSource> loadAllDataSource() {
Map<String, DataSource> map = new HashMap<>();
Map<String, Map<String, String>> ds = druidProperties.getDs();
try {
Set<String> keySet = ds.keySet();
for (String key : keySet) {
map.put(key, druidProperties.datasource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
6、model包下 --User
package org.datasource.zz.model;
/**
* @author 81230
*/
public class User {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
7、mapper包下 --UserMapper
package org.datasource.zz.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.datasource.zz.model.User;
import java.util.List;
/**
* @author 81230
*/
@Mapper
public interface UserMapper {
@Select("select * from user")
List<User> getAllUsers();
}
8、service包下 --UserMapper
package org.datasource.zz.service;
import org.datasource.zz.annotation.DataSource;
import org.datasource.zz.mapper.UserMapper;
import org.datasource.zz.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
UserMapper userMapper;
@DataSource("master")
public List<User> getAllUsers() {
return userMapper.getAllUsers();
}
}
9、controller包下 --DataSourceController
package org.datasource.zz.controller;
import org.datasource.zz.datasource.DataSourceType;
import org.datasource.zz.model.User;
import org.datasource.zz.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.List;
/**
* @author 81230
*/
@RestController
public class DataSourceController {
private static final Logger logger = LoggerFactory.getLogger(DataSourceController.class);
@Autowired
UserService userService;
/**
* 修改数据源
* @param dsType
*/
@PostMapping("/dstype")
public void setDsType(String dsType, HttpSession session){
session.setAttribute(DataSourceType.DS_SESSION_KEY,dsType);
logger.info("数据源切换{}",dsType);
}
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
}