当前位置: 首页 > news >正文

SpringBoot+Aop+注解方式 实现多数据源动态切换

整体思路:

  1. 引入基本依赖SpringBoot+Aop+MySql+MyBatis+lombok
  2. 在配置文件中配置多个数据源
  3. 创建数据源配置类用于读取配置
  4. 编写用于标识切换数据源的注解
  5. 创建数据源切换工具类DataSourceContextHolder
  6. 编写切面类用于在注解生效处切换数据源
  7. 编写配置类,加载数据源
  8. 创建动态数据源类,并继承AbstractRoutingDataSource,指定使用哪个数据源(关键)

项目demo gitee地址:多数据源动态切换demo

1.引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.10</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><version>2.7.10</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.21</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.3</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.20</version></dependency>

2.在配置文件中配置多个数据源

这里配置了上海,深圳,北京3个数据源,需要自己创建这3个库multi-sh,multi-sz,multi-bj

#默认数据源
datasource.default=sh
#上海库
spring.datasource.sh.url=jdbc:mysql://localhost:3306/multi-sh?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.sh.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.sh.username=root
spring.datasource.sh.password=123#深圳库
spring.datasource.sz.url=jdbc:mysql://localhost:3306/multi-sz?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.sz.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.sz.username=root
spring.datasource.sz.password=123#北京库
spring.datasource.bj.url=jdbc:mysql://localhost:3306/multi-bj?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.bj.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.bj.username=root
spring.datasource.bj.password=123

3.创建数据源配置类用于读取配置

spring获取统一前缀配置需要可以看我之前的文章:SpringBoot项目获取统一前缀配置以及获取非确定名称配置

package com.gooluke.datasource;import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;import java.util.Map;/*** @author gooluke*/
@Setter
@Getter
@Component
@ConfigurationProperties(prefix = "spring")
public class MultiDataSourceProperties {/*** 这里的datasource是因为配置是spring.datasource.xx.xx,要配置成datasource,这样才会把配置自动映射进来* 分别映射到url、driverClassName、username、password*/private Map<String, DataSourceConfig> datasource;@Setter@Getterpublic static class DataSourceConfig {private String url;private String driverClassName;private String username;private String password;}
}

4.编写用于标识切换数据源的注解

package com.gooluke.common.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author gooluke*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FixedDataSource {String value();/*** 是否需要还原回之前的数据源(拓展)*/boolean needRecover() default false;}

5.创建数据源切换工具类DataSourceContextHolder

package com.gooluke.datasource;import com.gooluke.config.DataSourceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @author gooluke* 将数据源信息存放至ThreadLocal*/
public class DatasourceContextHolder {private static final Logger log = LoggerFactory.getLogger(DatasourceContextHolder.class);private static final ThreadLocal<String> DATASOURCE_THREAD_LOCAL = new ThreadLocal<>();public static void setDatasource(String datasource) {if (datasource != null && DataSourceConfig.dataSources.get(datasource) == null) {String errorMsg = String.format("数据源[%s]未配置", datasource);log.error(errorMsg);throw new RuntimeException(errorMsg);}DATASOURCE_THREAD_LOCAL.set(datasource);}public static String getDatasource() {return DATASOURCE_THREAD_LOCAL.get();}public static void clearDatasource() {DATASOURCE_THREAD_LOCAL.remove();}
}

6.编写切面类用于在注解生效处切换数据源

package com.gooluke.aspect;import com.gooluke.common.annotation.FixedDataSource;
import com.gooluke.datasource.DatasourceContextHolder;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;/*** @author gooluke* 切换数据源切面类* 这个已不再使用,使用com.gooluke.aop.DataSourceAnnotationAdvisor替代*/
@Aspect
@Component
public class DataSourceAspect {private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);/*** 注解加在方法上*/@Pointcut("@annotation(com.gooluke.common.annotation.FixedDataSource)")private void methodPointCut() {}/*** 注解加在方法上*/@Pointcut("@within(com.gooluke.common.annotation.FixedDataSource)")public void classPointcut() {}@Around(value = "methodPointCut() || classPointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {//记录当前数据源,和准备切换的数据源String oldDatasource = DatasourceContextHolder.getDatasource();MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();java.lang.reflect.Method method = methodSignature.getMethod();FixedDataSource annotation = method.getAnnotation(FixedDataSource.class);//方法上获取注解为空,再从类上获取if (annotation == null) {annotation = method.getDeclaringClass().getAnnotation(FixedDataSource.class);}String newDatasource = annotation.value();//切换数据源,并执行操作DatasourceContextHolder.setDatasource(newDatasource);try {return joinPoint.proceed();} finally {//是否切换回初始数据源if (annotation.needRecover()) {DatasourceContextHolder.setDatasource(oldDatasource);}}}
}

7.编写配置类,加载数据源

这个配置类,主要就是将我们配置的多数据源解析然后统一管理,dynamicDataSource.setTargetDataSources(targetDataSources); 以及设置默认数据源。

package com.gooluke.config;import com.alibaba.druid.pool.DruidDataSource;
import com.gooluke.aop.DataSourceAnnotationAdvisor;
import com.gooluke.aop.DataSourceAnnotationInterceptor;
import com.gooluke.datasource.DynamicDataSource;
import com.gooluke.datasource.MultiDataSourceProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 gooluke*/@Configuration
@Slf4j
public class DataSourceConfig {public static final Map<String, String> dataSources = new HashMap<>();@AutowiredMultiDataSourceProperties dataSourceProperties;@Value("${datasource.default:}")private String defaultDataSourceName;@Bean@Primarypublic DynamicDataSource dynamicDataSource() {DynamicDataSource dynamicDataSource = new DynamicDataSource();//存放所有数据源Map<Object, Object> targetDataSources = new HashMap<>();Map<String, MultiDataSourceProperties.DataSourceConfig> datasourceMap = dataSourceProperties.getDatasource();if (datasourceMap.entrySet().size() > 1 && (defaultDataSourceName == null || defaultDataSourceName.isEmpty())) {throw new RuntimeException("存在多个数据源,未配置默认数据源:datasource.default");}datasourceMap.forEach((datasourceName, config) -> {DataSource dataSource = createDataSource(config);targetDataSources.put(datasourceName, dataSource);dataSources.put(datasourceName, datasourceName);log.info("已初始化数据库:{}", datasourceName);if (datasourceMap.size() == 1 || (defaultDataSourceName != null && !defaultDataSourceName.isEmpty() && defaultDataSourceName.equals(datasourceName))) {//这里设置默认数据源dynamicDataSource.setDefaultTargetDataSource(dataSource);log.info("已设置默认数据源: {}", datasourceName);}});//这里把数据源统一管理dynamicDataSource.setTargetDataSources(targetDataSources);return dynamicDataSource;}private DataSource createDataSource(MultiDataSourceProperties.DataSourceConfig dataSourceConfig) {DruidDataSource dataSource = new DruidDataSource();dataSource.setUrl(dataSourceConfig.getUrl());dataSource.setDriverClassName(dataSourceConfig.getDriverClassName());dataSource.setUsername(dataSourceConfig.getUsername());dataSource.setPassword(dataSourceConfig.getPassword());dataSource.setValidationQuery("SELECT 1");dataSource.setTestWhileIdle(true);dataSource.setTestOnBorrow(false);dataSource.setTestOnReturn(false);dataSource.setPoolPreparedStatements(true);dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);return dataSource;}@Bean@Primarypublic SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();sessionFactory.setDataSource(dynamicDataSource);PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();org.springframework.core.io.Resource[] resources = resolver.getResources("classpath:/mapper/*.xml");//org.springframework.core.io.Resource config = resolver.getResource("classpath:mybatis-config.xml");sessionFactory.setMapperLocations(resources);//sessionFactory.setConfigLocation(config);return sessionFactory.getObject();}@Bean@Primarypublic DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {return new DataSourceTransactionManager(dynamicDataSource);}}

8.创建动态数据源类,并继承AbstractRoutingDataSource,指定使用哪个数据源(关键)

这里可以理解为就是一个口子,让我们自己指定数据源,如果你返回的是null,则会指定我们配置类中设置的默认数据源:dynamicDataSource.setDefaultTargetDataSource(dataSource);

package com.gooluke.datasource;import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;/*** @author gooluke* 动态数据源*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 这里返回ThreadLocal中的数据源*/@Overrideprotected Object determineCurrentLookupKey() {return DatasourceContextHolder.getDatasource();}}

9.请求完成后,记得清空ThreadLocal,否则会造成内存泄漏

编写一个拦截器,在请求完成后,remove

package com.gooluke.interceptor;import com.gooluke.datasource.DatasourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @author gooluke*/
@Component
@Slf4j
public class DataSourceInterceptor implements HandlerInterceptor {@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {DatasourceContextHolder.clearDatasource();log.info("请求处理完成,清除数据源");}
}

10.代码演示

将注解加在实现类方法上,或者加在mapper/dao接口上(一般加在这里,因为dao接口一般都是操作同一个库,这里指定了,其它别的方法直接调用即可)

10.1 service层:

package com.gooluke.service.impl;import com.gooluke.dao.UserInfoDao;
import com.gooluke.dao.UserInfoDao2;
import com.gooluke.entity.TUserInfo;
import com.gooluke.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** @author gooluke*/
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserInfoDao userInfoDao;@Autowiredprivate UserInfoDao2 userInfoDao2;/*** 在这里没有设置数据源,dao层设置了数据源,可以自动切换*/@Overridepublic List<TUserInfo> selectList() {//先查深圳库,再查上海库List<TUserInfo> tUserInfos = userInfoDao.selectUserList(new TUserInfo());tUserInfos.forEach(System.out::println);List<TUserInfo> tUserInfos2 = userInfoDao2.selectUserList(new TUserInfo());tUserInfos2.forEach(System.out::println);tUserInfos.addAll(tUserInfos2);return tUserInfos;}}

10.2 dao层:

dao1指定深圳库:

package com.gooluke.dao;import com.gooluke.common.annotation.FixedDataSource;
import com.gooluke.common.constants.DataSourceName;
import com.gooluke.entity.TUserInfo;
import org.apache.ibatis.annotations.Mapper;import java.util.List;/*** @author gooluke*/
@Mapper
@FixedDataSource(DataSourceName.SHENZHEN)
public interface UserInfoDao {List<TUserInfo> selectUserList(TUserInfo userInfo);}

dao2指定上海库:

package com.gooluke.dao;import com.gooluke.common.annotation.FixedDataSource;
import com.gooluke.common.constants.DataSourceName;
import com.gooluke.entity.TUserInfo;
import org.apache.ibatis.annotations.Mapper;import java.util.List;/*** @author gooluke*/
@Mapper
@FixedDataSource(DataSourceName.SHANGHAI)
public interface UserInfoDao2 {List<TUserInfo> selectUserList(TUserInfo userInfo);}

10.3 观察结果

切库成功,分别查询了不同库的数据,并在最后清空了ThreadLocal中的数据

11.动态数据源(开源)dynamic-datasource-spring-boot-starter

上面这种Aop的实现方式在注解加在service接口的方法上其实是不生效的,当然也不建议加在service接口上,通常是加在实现类类上或者方法上。而Mapper/Dao接口的实现类是通过mybatis动态代理生成的,注解加在Mapper/Dao接口上是能生效的,我没有找到为啥他的实现类可以的文章。而我们也可以通过别的方式,把注解加在接口上的场景通过Aop拦截,只是不建议。下面是开源组件-动态数据源

'com.baomidou:dynamic-datasource-spring-boot-starter:3.3.2'

的Aop方案,有兴趣的可以去看一下他的源码,我的工程里也是用的这种方案,需要在配置类中声明@bean

11.1 创建一个DataSourceAnnotationAdvisor去继承AbstractPointcutAdvisor类,并实现BeanFactoryAware接口

11.2 重写getPointcut()、getAdvice()、setBeanFactory()方法

11.3 配置声明@Bean

    @Beanpublic DataSourceAnnotationAdvisor dataSourceAnnotationAdvisor() {DataSourceAnnotationInterceptor dataSourceAnnotationInterceptor = new DataSourceAnnotationInterceptor();return new DataSourceAnnotationAdvisor(dataSourceAnnotationInterceptor);}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • [游戏技术]L4D服务器报错解决
  • 31省市农业地图大数据
  • 开源RK3588 AI Module7,并与Jetson Nano生态兼容的低功耗AI模块
  • Django学习实战篇四(适合略有基础的新手小白学习)(从0开发项目)
  • 零工市场小程序:推动零工市场建设
  • MySQL Performance Schema 详解及运行时配置优化
  • 计算机前沿技术-人工智能算法-大语言模型-最新论文阅读-2024-09-20
  • Nginx 反向代理
  • 随手记:前端一些定位bug的方法
  • 大语言模型量化方法GPTQ、GGUF、AWQ详细原理
  • 全栈开发(一):springBoot3+mysql初始化
  • 邮件发送高级功能详解:HTML格式、附件添加与SSL/TLS加密连接
  • 提升工作效率,引领编程新时代
  • 抽象类 vs 接口:它们有何异同?
  • 智能算法躲避拥堵,高德企业用车上线“动态选路服务”为出行提效
  • JS 中的深拷贝与浅拷贝
  • 【跃迁之路】【641天】程序员高效学习方法论探索系列(实验阶段398-2018.11.14)...
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • DataBase in Android
  • Docker 笔记(2):Dockerfile
  • Docker入门(二) - Dockerfile
  • Elasticsearch 参考指南(升级前重新索引)
  • iOS小技巧之UIImagePickerController实现头像选择
  • Joomla 2.x, 3.x useful code cheatsheet
  • SpriteKit 技巧之添加背景图片
  • Vue2.x学习三:事件处理生命周期钩子
  • windows下如何用phpstorm同步测试服务器
  • Zsh 开发指南(第十四篇 文件读写)
  • 从零开始的无人驾驶 1
  • 第十八天-企业应用架构模式-基本模式
  • 关键词挖掘技术哪家强(一)基于node.js技术开发一个关键字查询工具
  • 关于使用markdown的方法(引自CSDN教程)
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 前端面试总结(at, md)
  • 如何使用Mybatis第三方插件--PageHelper实现分页操作
  • 3月27日云栖精选夜读 | 从 “城市大脑”实践,瞭望未来城市源起 ...
  • ​2021半年盘点,不想你错过的重磅新书
  • !!java web学习笔记(一到五)
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (C#)一个最简单的链表类
  • (C++17) std算法之执行策略 execution
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (三)mysql_MYSQL(三)
  • (三)终结任务
  • (删)Java线程同步实现一:synchronzied和wait()/notify()
  • (一)RocketMQ初步认识
  • .env.development、.env.production、.env.staging
  • .form文件_一篇文章学会文件上传
  • .net 4.0 A potentially dangerous Request.Form value was detected from the client 的解决方案
  • .NET Core日志内容详解,详解不同日志级别的区别和有关日志记录的实用工具和第三方库详解与示例
  • .NET Project Open Day(2011.11.13)
  • .net快速开发框架源码分享
  • .NET设计模式(8):适配器模式(Adapter Pattern)
  • .Net中间语言BeforeFieldInit
  • @Autowired @Resource @Qualifier的区别