本篇博客基于SpringBoot整合MyBatis-plus,如果有不懂这个的,
可以查看我的这篇博客:快速CRUD的秘诀之SpringBoot整合MyBatis-Plus
MyBatis-Plus官方推荐的简单切换数据源,可以参考上期博客:SpringBoot快速集成多数据源(自动版)
那么问题来了?我上期博客已经实现过一次多数据源切换了,并且非常简单,为什么还要出一期定制版呢?
是这样的,在上一期博客中,通过@DS
注解在不同的方法上,然后再通过调用不同的方法来实现调用不同的数据源:
那么,如果有8个数据源呢?是不是要写8个方法,8个判断呢?
所以,到底有没有方法能够简化代码呢?反正切换大概都是一样的逻辑。
本博主灵光一现,这篇博客也就应运而生了!
当然,如果已经规定好了哪些业务用哪些数据源,不存在同一个业务要根据不同的用户来切换的烦恼,您就用官方推荐的方式吧,甭折腾了!
跟上期同样,为了实现效果,先在本地的mysql库里面创建两个数据库:
然后在两个数据库里面,分别创建同样的users表,但是插入不同的数据,
mydb的数据:
mydb2的数据:
1.在pom.xml
文件中引入AOP所需的依赖aspectjweaver
:
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.7version>
dependency>
2.在application.yml
中进行配置:
server:
port: 8080
# 配置数据源
spring:
datasource:
master:
# 数据库路径jdbc:mysql://localhost:3306/mydb 的缩写,并配置时区
jdbc-url: jdbc:mysql:///mydb?serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave:
jdbc-url: jdbc:mysql:///mydb2?serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 打印MyBatis SQL 日志
logging:
level:
com.guqueyue.dynamicdatasourcetest.dao: debug # 写接口的包名
这里需要注意的是数据源的路径名不能用url
,要用jdbc-url
!!!
3.编写DataSourceType
类,用以设置数据源:
package com.guqueyue.dynamicdatasourcetest.config;
import lombok.extern.slf4j.Slf4j;
/**
* @Author: guqueyue
* @Description: 用以设置数据源
* @Date: 2023/12/25
**/
@Slf4j
public class DataSourceType {
// 内部枚举类,用以选择特定的数据源
public enum DataBaseType {
master,
slave
}
// 使用ThreadLocal保证线程安全
private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<>();
// 往当前线程中设置数据源类型
public static void setDataBaseType(DataBaseType dataBaseType) {
if (dataBaseType == null) {
throw new NullPointerException();
}
TYPE.set(dataBaseType);
log.info("\033[31;0;42;30;1;2;3;4;41m" + "线程:[" + Thread.currentThread().getName() +"],取了[" + getDataBaseType() + "]=>这个数据源");
}
// 获取数据源类型
public static DataBaseType getDataBaseType() {
return TYPE.get() == null ? DataBaseType.master : TYPE.get();
}
// 清空数据源类型
public static void clearDataBaseType() {
TYPE.remove();
}
}
4.扩展determineCurrentLookupKey()
方法来实现数据源的切换:
package com.guqueyue.dynamicdatasourcetest.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Author: guqueyue
* @Description: 通过扩展determineCurrentLookupKey()方法来实现数据源的切换
* @Date: 2023/12/25
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceType.getDataBaseType();
}
}
5.重头戏,编写多数据源配置类DynamicDataSourceConfig
:
package com.guqueyue.dynamicdatasourcetest.config;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: guqueyue
* @Description: 多数据源配置类
* @Date: 2023/12/25
**/
@Configuration
@MapperScan(basePackages = "com.guqueyue.dynamicdatasourcetest.dao", sqlSessionFactoryRef = "SqlSessionFactory") // basePackages为接口地址
public class DynamicDataSourceConfig {
// 将这个对象放入Spring容器中
@Bean(name = "masterDataSource")
// 表示这个数据源为默认数据源
@Primary
// 读取 application.yml 中的配置参数映射成为一个对象
// prefix表示配置文件中参数的前缀
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource getMasterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource getSlaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) {
// 核心点: targetDataSource为 数据源和注入的Bean 之间的映射
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceType.DataBaseType.master, masterDataSource);
targetDataSource.put(DataSourceType.DataBaseType.slave, slaveDataSource);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(masterDataSource); // 设置默认数据源
return dataSource;
}
@Bean(name = "SqlSessionFactory")
@DependsOn("globalConfig") // 明确调用顺序,在调用本方法前先调用 globalConfig() 方法
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
// SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
// 设置xml文件路径
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/guqueyue/dynamicdatasourcetest/dao/*.xml"));
// 设置别名的扫描 - 实体类的包名引用
bean.setTypeAliasesPackage("com.guqueyue.dynamicdatasourcetest.entity");
bean.setGlobalConfig(globalConfig());
return bean.getObject();
}
/**
* @Description 解决Oracle数据库下,用 @KeySequence注解 实现主键序列自增失效 问题解决
* @Param []
* @return com.baomidou.mybatisplus.core.config.GlobalConfig
**/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setDbConfig(new GlobalConfig.DbConfig().setKeyGenerator(new OracleKeyGenerator()));
return globalConfig;
}
@Bean(name = "sqlSessionTemplate")
@Primary // 首选
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* @Description 事务管理
* @Param [dataSource]
* @return org.springframework.jdbc.datasource.DataSourceTransactionManager
**/
@Bean(name = "dataSourceTransactionManager")
@Primary // 首选
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
6.编写自定义注解
关于自定义注解的了解,可以参考我的这篇博客:框架的灵魂之注解基础篇
package com.guqueyue.dynamicdatasourcetest.annotation;
import java.lang.annotation.*;
/**
* @Author: guqueyue
* @Description: 自定义注解,用以切换数据源
* @Date: 2023/12/25
**/
@Target({ElementType.METHOD}) // 注解使用范围为:方法
@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期为运行时
@Documented
public @interface MyDataSource {
// DataSourceType.DataBaseType value() default DataSourceType.DataBaseType.master; // 默认使用master库
}
7.进行自定义注解拦截,使用AOP来增强对象:
这里有一个核心逻辑,我们根据@MyDataSource
自定义注解拦截方法,然后根据方法所传的第一个参数值来判断使用哪个数据源。
这样我们就不用编写很多方法了。当然,如果没找到,就默认为主数据源。
package com.guqueyue.dynamicdatasourcetest.aop;
import com.guqueyue.dynamicdatasourcetest.annotation.MyDataSource;
import com.guqueyue.dynamicdatasourcetest.config.DataSourceType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* @Author: guqueyue
* @Description: 自定义注解拦截,使用AOP来增强对象
* @Date: 2023/12/25
**/
@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect {
// 拦截自定义注解, @annotation()里面是注解的路径,需要根据你自己的替换
@Before("@annotation(com.guqueyue.dynamicdatasourcetest.annotation.MyDataSource)")
public void changeDataSource(JoinPoint point) {
// // 获取增强方法
// MethodSignature methodSignature = (MethodSignature) point.getSignature();
// // 获取增强方法的反射对象
// Method method = methodSignature.getMethod();
// // 获取@MyDataSource注解
// MyDataSource myDataSource = method.getAnnotation(MyDataSource.class);
String dataSourceType = "master";
// 获取方法的第一个参数
Object[] args = point.getArgs();
if (args != null && args.length > 0) {
dataSourceType = args[0].toString();
}
// 选择数据源
switch (dataSourceType) {
case "slave":
DataSourceType.setDataBaseType(DataSourceType.DataBaseType.slave);
break;
default: // 默认为主数据源
DataSourceType.setDataBaseType(DataSourceType.DataBaseType.master);
break;
}
}
// 在方法执行完之后清除特定数据源的配置,使用默认数据源
@After("@annotation(myDataSource)")
public void restoreDataSource(JoinPoint point, MyDataSource myDataSource) {
DataSourceType.clearDataBaseType();
}
}
1.控制层代码
package com.guqueyue.dynamicdatasourcetest.controller;
import com.guqueyue.dynamicdatasourcetest.entity.User;
import com.guqueyue.dynamicdatasourcetest.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @Author: guqueyue
* @Description: 用户控制层
* @Date: 2023/12/25
**/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
/**
* 查询用户列表
* @return
*/
@RequestMapping("/list")
public List<User> userList(String type) {
return userService.selectUserList(type);
}
}
2.service接口
package com.guqueyue.dynamicdatasourcetest.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.guqueyue.dynamicdatasourcetest.entity.User;
import java.util.List;
/**
* @Author: guqueyue
* @Description: 用户service接口
* @Date: 2023/12/25
**/
public interface IUserService extends IService<User> {
List<User> selectUserList(String type);
}
3.service实现类
package com.guqueyue.dynamicdatasourcetest.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guqueyue.dynamicdatasourcetest.annotation.MyDataSource;
import com.guqueyue.dynamicdatasourcetest.dao.UserMapper;
import com.guqueyue.dynamicdatasourcetest.entity.User;
import com.guqueyue.dynamicdatasourcetest.service.IUserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author: guqueyue
* @Description: 用户实现类
* @Date: 2023/12/25
**/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private UserMapper userMapper;
@Override
public List<User> selectUserList(String type) {
// do something: 此处可根据实际情况进行业务选择,如根据当前登录的用户信息来选择不同的数据库
// 此处为了方便演示,直接用前端传入的type来判断
return userMapper.selectUserList(type);
}
}
4.dao层
package com.guqueyue.dynamicdatasourcetest.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.guqueyue.dynamicdatasourcetest.annotation.MyDataSource;
import com.guqueyue.dynamicdatasourcetest.entity.User;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @Author: guqueyue
* @Description: 映射接口UserMapper
* @Date: 2023/12/25
**/
public interface UserMapper extends BaseMapper<User> {
@MyDataSource
@Select("SELECT id,password,username FROM users")
List<User> selectUserList(String type);
}
5.启动项目,看效果
根据之前自定义注解拦截
时编写的逻辑,我们会根据selectUserList()
方法中的第一个参数type
来决定取哪个数据源。
在控制台看到tomcat在你配置的端口号上启动了,说明项目启动成功了!
Tomcat started on port(s): 8080 (http) with context path ''
在浏览器输入:http://localhost:8080/user/list?type=master
,
可以看到控制台打印:
浏览器返回:
再在浏览器输入:http://localhost:8080/user/list?type=slave
,
可以看到控制台打印:
浏览器返回:
说明成功切换了不同的数据源!
本文在演示的时候,在dao层使用的是@Select
注解来执行SQL语句,并没有使用xml文件。
如需使用xml文件执行SQL语句的话,
可以参照我的这篇博客:快速CRUD的秘诀之SpringBoot整合MyBatis-Plus 的扩展部分。
但不同的是,需要在多数据源配置类DynamicDataSourceConfig
的sqlSessionFactory()
方法中进行配置,而不是applicaton.yml
配置文件中:
@Bean(name = "SqlSessionFactory")
@DependsOn("globalConfig") // 明确调用顺序,在调用本方法前先调用 globalConfig() 方法
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
// SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
// 设置xml文件路径
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/guqueyue/dynamicdatasourcetest/dao/*.xml"));
// 设置别名的扫描 - 实体类的包名引用
bean.setTypeAliasesPackage("com.guqueyue.dynamicdatasourcetest.entity");
bean.setGlobalConfig(globalConfig());
return bean.getObject();
}
将这两行代码根据你的实际情况修改即可:
// 设置xml文件路径
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/guqueyue/dynamicdatasourcetest/dao/*.xml"));
// 设置别名的扫描 - 实体类的包名引用
bean.setTypeAliasesPackage("com.guqueyue.dynamicdatasourcetest.entity");
我们下期博客再见!