自定义多数据源并使用注解动态切换
2022年11月1日
自定义多数据源实现思路
- 自定义注解
@DataSource
,可以加在Service层的类上或者方法上,并设置一个默认数据源(master)- 加在类上,代表该类下的所有方法都使用该数据源
- 加在方法上,表示该方法使用该数据源。
- 自定义切面,定义切点(所有方法和类上加了
@DataSource
注解)和环绕通知,先获取方法上加了@DataSource
,如果没有获取到,则获取类上的,即方法上的优先级高于类上的优先级。接着获取到注解上指定数据源值,并将此数据源值存到ThreadLocal
中。 - 每次Mapper执行的时候,JDBC会使用
AbstractRoutingDataSource
实例中的determineCurrentLookupKey
方法得到指定数据源。所有只需要在该实例注入到时候设置所有的数据源信息(key为yaml中配置的一级属性,value为属性配置对应的数据源信息)和默认的数据源信息,然后通过重写determineCurrentLookupKey
方法,返回ThreadLocal
对应的数据源值即可
实现通过页面参数实现切换数据源思路
客户端在进行页面数据源切换操作时,需要请求服务器先把客户端指定的数据源值存在一个位置,方便后续切面通过此位置拿到数据源信息,这里可以选择session,redis等位置。这里选择将客户端传来的数据源值存在session中。
然后需要增加一个切面即可:先定义切面,切点范围为所有Service下的所有类和方法,定义环绕通知,拦截方法后首先拿到上一步存在Session中的数据源信息存入ThreadLocal。
怎么保证内部两个切面的优先级
- 可以通过
@Order
注解指定切面执行的优先级,若@Order值小的则会先执行,@Order大的会后执行,那么在对数据源信息进行存储在ThreadLocal时,先执行的值会被后执行的覆盖。 - 所有可以通过指定
@Order
注解,值大的,切面最后执行,最终配置优先级最高。
案例
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<dependency>
<groupId>org.lc.demo</groupId>
<artifactId>spring-demo-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
yaml配置
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
ds:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
# 从库数据源
slave:
url: jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
# 其他数据源1,2....n
#以下是公共配置
# 初始连接数
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
数据源配置
常量配置
/**
* 公共的常量配置信息
*/
public class DataSourceConstantType {
//默认的数据源的key
public static final String DEFAULT_DATASOURCE = "master";
//将要切换对数据源信息保存在session中key
public static final String DS_SESSION_KEY = "ds_session_key";
}
数据源属性配置
@ConfigurationProperties(prefix = "spring.datasource")
@Data
public class DruidProperties {
private String type;
private String driverClassName;
/**
* 这里存储多个数据源的主要配置url、username、password。当然如果有其他配置也可以加入进来
*/
private Map<String, Map<String, String>> ds;
//初始连接数
private Integer initialSize;
//最小连接池数量
private Integer minIdle;
//最大连接池数量
private Integer maxActive;
//配置获取连接等待超时的时间
private Integer maxWait;
//
//...省略其他公共配置
//
/**
* 从外部构造一个DruidDataSource对象传进来:只包含 url、username、password。
* 从外部将DruidDataSource传入该方法后相当于给每个数据源设置相同的公共配置
*/
public DataSource setDataSource(DruidDataSource druidDataSource) {
druidDataSource.setInitialSize(initialSize);
druidDataSource.setMinIdle(minIdle);
druidDataSource.setMaxActive(maxActive);
druidDataSource.setMaxWait(maxWait);
return druidDataSource;
}
}
动态数据源上下文线程配置
/**
* 此方法可以用于将每个线程的使用的数据源存入到ThreadLocal中
*/
public class DynamicDataSourceContextHolder {
private static ThreadLocal<String> CONTEXT_HOLDER=new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void removeDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
加载整合数据源工具类配置
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
@Autowired
public DruidProperties druidProperties;
public Map<String, DataSource> loadAllDataSource() {
//处理完之后的完整的数据源信息集合
Map<String, DataSource> completeDataSource = new HashMap<>();
//获取所有的数据源配置信息
Map<String, Map<String, String>> ds = druidProperties.getDs();
try {
Set<String> keyDataSource = ds.keySet();
//遍历所有数据源
for (String dataSource : keyDataSource) {
//将map中的数据源信息转换为DruidDataSource对象
DruidDataSource druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(dataSource));
//补全DruidDataSource中其他的公共配置信息
completeDataSource.put(dataSource, druidProperties.setDataSource(druidDataSource));
}
} catch (Exception e) {
e.printStackTrace();
}
return completeDataSource;
}
}
JDBC执行动态查找数据源配置
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 这里使用构造器注入的安全方式。
* 如果使用@Autowired方式注入,则注入为null. 因为构造器先于属性filed注入方式执行
*/
public DynamicDataSource(LoadDataSource loadDataSource) {
//1.获取所有数据源
Map<String, DataSource> allDataSource = loadDataSource.loadAllDataSource();
//设置查找的所有数据源信息
super.setTargetDataSources(new HashMap<>(allDataSource));
//2.设置默认的数据源信息
super.setDefaultTargetDataSource(allDataSource.get(DataSourceConstantType.DEFAULT_DATASOURCE));
super.afterPropertiesSet();
}
/**
* Mapper去查询数据库前,会先动态调用此方法获取数据源的key决定使用哪个数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
自定义注解
/**
* 可以将该注解加在Service层上的方法或者类上,表明使用哪个数据源
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DataSource {
//如果没有指定数据源信息,则默认使用master配置
String value() default DataSourceConstantType.DEFAULT_DATASOURCE;
}
切面配置
自定义获取注解切面
@Component
@Aspect
@Order(10)
public class DataSourceAspect {
/**
* 定义切点,什么条件会拦截
* @annotation(org.lc.demo.annocation.DataSource) 表示加了@DataSource注解的方法会拦截
* @within(org.lc.demo.annocation.DataSource) 表示加了@DataSource注解的类会拦截
*/
@Pointcut("@annotation(org.lc.demo.annocation.DataSource) || @within(org.lc.demo.annocation.DataSource)")
public void pointcutDataSource() { }
/**
* 定义环绕通知
*/
@Around("pointcutDataSource()")
public Object around(ProceedingJoinPoint joinPoint) {
//获取方法和类上的@DataSource注解
DataSource dataSource=getDataSource(joinPoint);
if (dataSource != null) {
String value = dataSource.value();
//将拦截到的数据源的值存储到ThreadLocal中
DynamicDataSourceContextHolder.setDataSourceType(value);
}
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
DynamicDataSourceContextHolder.removeDataSourceType();
}
return null;
}
/**
* 获取方法或者类上的@DataSource注解
*/
public DataSource getDataSource(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//先查找方法上的@DataSource注解(代表方法上的优先级高于类上的优先级)
DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (annotation == null) {
//若方法上找不到@DataSource注解,则去类上找。
annotation=AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
return annotation;
}
}
自定义session获取切面
@Component
@Aspect
@Order(11)
public class GlobalDataSourceAspect {
@Autowired
private HttpSession httpSession;
/**
* 对org.lc.demo.service对所有类下对所有方法进行拦截
*/
@Pointcut("execution(* org.lc.demo.service.*.*(..))")
public void pc() {
}
@Around("pc()")
public Object around(ProceedingJoinPoint pjp) {
//从session中拿到切换数据源信息存入到ThreadLocal中
DynamicDataSourceContextHolder.setDataSourceType((String) httpSession.getAttribute(DataSourceConstantType.DS_SESSION_KEY));
Object result=null;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
DynamicDataSourceContextHolder.removeDataSourceType();
}
return result;
}
}
实体
@Data
public class Person {
private String id;
private String name;
private Integer age;
}
mapper
@Mapper
public interface PersonMapper {
@Select("select * from person")
List<Person> getAllPerson();
}
service
@Service
//默认所有方法使用主数据源
// @DataSource
public class PersonService {
@Autowired
private PersonMapper personMapper;
//方法上的优先级高于类上使用的数据源。并且生效的数据源根据两个切面配置的最终执行顺序有关
//@DataSource("slave")
public List<Person> getAllPerson() {
return personMapper.getAllPerson();
}
}
controller
@RestController
public class DataSourceChangeController {
private static final Logger logger = LoggerFactory.getLogger(DataSourceChangeController.class);
@Autowired
private PersonService personService;
/**
* 切换数据源
* @param dsType 指定数据源
* @param httpSession
*/
@PostMapping("/changeDataSource")
public void changeDataSource(String dsType, HttpSession httpSession) {
httpSession.setAttribute(DataSourceConstantType.DS_SESSION_KEY, dsType);
logger.info("数据源切换成功:{}", dsType);
}
/**
* 获取数据
* @return
*/
@PostMapping("/getAllPerson")
public List<Person> getAllPerson() {
return personService.getAllPerson();
}
}
页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<el-tag>切换数据源:</el-tag>
<el-select v-model="value" placeholder="请选择" @change="changeDataSource">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<el-button type="primary" @click="loadDataSource">加载数据</el-button>
<el-input
type="textarea"
:rows="2"
placeholder="数据内容"
v-model="textarea">
</el-input>
</div>
<script>
new Vue({
el: '#app',
data: {
value:'master',
textarea:'',
options:[
{label:"主数据源",value:'master'},
{label:"从数据源",value:'slave'}
]
},
methods:{
changeDataSource(value) {
axios({
method: 'post',
url: '/changeDataSource',
params: {
dsType: value
}
})
},
loadDataSource() {
axios({
method: 'post',
url: '/getAllPerson'
}).then(res=>{
this.textarea = JSON.stringify(res.data);
})
}
}
})
</script>
</body>
</html>
测试结果分析
根据上述AOP切面信息,DataSourceAspect切面先于GlobalDataSourceAspect先执行,所以最终生效的的配置为GlobalDataSourceAspect中指定的通知,也就是存在在ThreadLocal中的数据源信息被后者覆盖了,也就是执行的方法以执行/changeDataSource
请求传过来存在session中的数据源信息为准。如果想要以方法或者类上的结果为准,那么则改变切面执行的顺序即可,也就是GlobalDataSourceAspect切面先执行,DataSourceAspect切面后执行
- 主数据信息
- 从数据源信息