• 实战:Spring AOP实现多数据源动态切换


    需求背景


    去年底,公司项目有一个需求中有个接口需要用到平台、算法、大数据等三个不同数据库的数据进行计算、组装以及最后的展示,当时这个需求是另一个老同事在做,我只是负责自己的部分。
    直到今年回来了,这个项目也做得差不多了,这会儿才有时间区仔细看同事的代码,是怎么去实现多数据源动态切换的。


    扩展:当业务也来越复杂,数据量越来越庞大时,就可能会对数据库进行分库分表、读写分离等设计来减轻压力、提高系统性能,那么多数据源动态切换势必是必不可少!

    经过了一星期零零碎碎的下班时间,从了解原理、实现、优化的过程,自己终于总算是弄出来了,接下来一起看看!

    思考


    1. 如何让Spring知道我们配置了多个数据源?

    2. 配置了多个数据源后,Spring是如何决定使用哪一个数据源?

    3. Spring是如何动态切换数据源?

    分析及实现


    1. 配置多数据源信息

    spring:
      datasource:
        local:
          database: local
          username: root
          password: 
          jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
          driver-class-name: com.mysql.cj.jdbc.Driver
        server:
          database: server
          username: root
          password: 
          jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
          driver-class-name: com.mysql.cj.jdbc.Driver
    

    这是我的两个数据库:本地数据库+个人服务器数据库


    服务器数据库



    本地数据库




    1. Spring如何获取配置好的多个数据源信息?

    Spring提供了三种方式进行获取

    @Value注解获取(实体类需配合@Component),最简单,但当配置信息较多时,写起来比较繁琐

    @ConfigurationProperties注解获取,需要定义前缀,可大批量获取配置信息

    @Environment注解从Spring环境中获取,实现较为复杂,本人很少用


    同事使用的方式是第一种方式,但是我个人觉得这样侵入性较大,每增加一个数据源,就要重新定义变量然后用@Value去重新配置,很麻烦,所以我就选择了第二种方式


    通过@ConfigurationProperties注解获取,需要定义前缀,可大批量获取配置信息


    @Data
    @Component
    @ConfigurationProperties(prefix = "spring.datasource")
    public class DBProperties {
    
        private HikariDataSource server;
    
        private HikariDataSource local;
    }
    

    将所有的数据源加载到Spring中,可供其选择使用


    @Slf4j
    @Configuration
    public class DataSourceConfig {
    
        @Autowired
        private DBProperties dbProperties;
    
        @Bean(name = "multiDataSource")
        public MultiDataSource multiDataSource(){
            MultiDataSource multiDataSource = new MultiDataSource();
            //1.设置默认数据源
            multiDataSource .setDefaultTargetDataSource(dbProperties.getLocal());
            //2.配置多数据源
            HashMap<Object, Object> dataSourceMap = Maps.newHashMap();
    
            dataSourceMap.put("local", dbProperties.getLocal());
            dataSourceMap.put("server", dbProperties.getServer());
            //3.存放数据源集
            multiDataSource.setTargetDataSources(dataSourceMap);
            return multiDataSource;
        }
    }
    

    如此之后,确实是可以读取YML中的数据源信息,但是总觉得怪怪的。
    果然!当我实现了整个功能后,我发现,如果我想要再加一个数据源,我还是得去求改DBProperties和DataSourceConfig这两类的内容,就很烦,我这个人比较懒,所以我就将这部分内容优化了一下:


    优化后的YML

    spring:
      datasource:
        names:
           - database: dataSource0
             username: root
             password: 
             jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
             driver-class-name: com.mysql.cj.jdbc.Driver
           - database: dataSource1
             username: root
             password: 
             jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
             driver-class-name: com.mysql.cj.jdbc.Driver
    

    优化后的DBProperties

    @Data
    @Component
    @ConfigurationProperties(prefix = "spring.datasource")
    public class DBProperties {
    
        private List<HikariDataSource> DBNames;
    
    }
    

    优化后的DataSourceConfig


    @Slf4j
    @Configuration
    public class DataSourceConfig {
    
        @Autowired
        private DBProperties dbProperties;
    
    
        @Bean(name = "multiDataSource")
        public MultiDataSource multiDataSource(){
            MultiDataSource multiDataSource = new MultiDataSource();
            
            List<HikariDataSource> names = dbProperties.getNames();
            if (CollectionUtils.isEmpty(names)){
                throw new RuntimeException(" please configure the data source! ");
            }
    
            multiDataSource.setDefaultTargetDataSource(names.get(0));
    
            HashMap<Object, Object> dataSourceMap = Maps.newHashMap();
            int i = 0;
            for (HikariDataSource name : names) {
                dataSourceMap.put("dataSource"+(i++),name);
            }
    
            multiDataSource.setTargetDataSources(dataSourceMap);
            return multiDataSource;
        }
    }
    

    这样子,我之后无论配置了多少个数据源信息,我都不需要再去修改配置代码



    1. Spring如何选择使用数据源?

    选择一个数据源


    通过继承AbstractRoutingDataSource接口,重写determineCurrentLookupKey方法,选择具体的数据源


    @Slf4j
    public class MultiDataSource extends AbstractRoutingDataSource {
        
        @Override
        protected Object determineCurrentLookupKey() {
    
            return MultiDataSourceHolder.getDatasource();
    
        }
        
    }
    

    利用ThreadLocal实现数据源线程隔离


    public class MultiDataSourceHolder {
    
        private static final ThreadLocal<String> threadLocal =new ThreadLocal<>();
    
        public static void setDatasource(String datasource){
            threadLocal.set(datasource);
        }
    
        public static String getDatasource(){
            return threadLocal.get();
        }
    
        public static void clearDataSource(){
            threadLocal.remove();
        }
    
    }
    

    准备工作做好,下面开始将动态切换操作串联起来


    利用AOP切面+自定义注解


    自定义注解


    @Target({ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface MultiDataSource {
    
        String DBName();
    
    }
    

    AOP切面


    @Slf4j
    @Aspect
    @Component
    public class DataSourceAspect {
    
        @Pointcut(value = "@within(com.xiaozhao.base.aop.annotation.MultiDataSource) || @annotation(com.xiaozhao.base.aop.annotation.MultiDataSource)")
        public void dataSourcePointCut(){}
    
    
        @Before("dataSourcePointCut() && @annotation(multiDataSource)")
        public void before(MultiDataSource multiDataSource){
    
            String dbName = multiDataSource.DBName();
    
            if (StringUtils.hasLength(dbName)){
    
                MultiDataSourceHolder.setDatasource(multiDataSource.DBName());
                log.info("current dataSourceName ====== "+dbName);
    
            }else {
    
                log.info("switch datasource fail, use default, or please configure the data source for the annotations,");
    
            }
        }
    
    
        @After("dataSourcePointCut()")
        public void after(){
            MultiDataSourceHolder.clearDataSource();
        }
    }
    

    好了!功能已然实现,打完收工!


    。。。。


    如果我工作中也这样,估计要被测试打死!为了敷衍一下,来进行一下测试


    一套代码直接打完:

    Controller+Service+Dao


    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
    
    
        @GetMapping("/info")
        public UserVO getUser(){
            return userService.creatUser();
        }
    }
    
    
    
    
    public interface UserService {
        UserVO creatUser();
    
        UserVO setUserInfo(String phone);
    }
    
    
    
    
    @Service
    @EnableAspectJAutoProxy(exposeProxy = true)
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private InfoMapper infoMapper;
    
    
        @Override
        public UserVO creatUser() {
            UserVO userVO = userMapper.getUserInfoMapper();
    
            return ((UserService) AopContext.currentProxy()).setUserInfo(userVO.getPhone());
        }
    
        @MultiDataSource(DBName = "dataSource1")
        public UserVO setUserInfo(String phone) {
    
            UserVO userInfo = infoMapper.getUserInfo();
    
            UserVO user = new UserVO();
            user.setUserName(userInfo.getUserName());
            user.setPassword(userInfo.getPassword());
            user.setAddress(userInfo.getAddress());
            user.setPhone(phone);
            return user;
        }
    }
    
    
    
    
    @Mapper
    public interface InfoMapper {
    
        @Select("select id,user_name as userName,password,phone,address from test_user")
        UserVO getUserInfo();
    }
    
    
    
    @Mapper
    public interface UserMapper {
    
        @Select("select id,user_name as userName,password,phone from user")
        UserVO getUserInfoMapper();
    
    }
    

    测试结果:红框数据来自于服务器数据库,绿框数据来自于本地数据库



    遇到的问题

    • 同一个类中,A方法调用B方法用AopContext.currentProxy()报错问题:在类上加@EnableAspectJAutoProxy(exposeProxy = true)————解决!
    • 配置多数据源时,注意将url修改成jdbc-url
    • 切面时,用JoinPoint获取方法,判断是否被注解修饰(虽然纯属多余)结果为false————有待考究!

    结语


    小菜鸡的学习成长之路,拒绝无味的CRUD,每过一段时间,就会把工作中用到,或者别人实现的功能解析、实现,并分享!下一篇,Redission实现分布式锁

  • 相关阅读:
    力扣第1488题——避免洪水泛滥
    欢迎使用Markdown编辑器
    什么是 X.509 证书以及它是如何工作的?
    Java易错知识点整理(待更新)
    ORACLE 查询SQL优化
    [office] EXCEL表格不能使用键盘箭头切换单元格该怎么解决- #媒体#经验分享#知识分享
    软考 系统架构设计师 简明教程 | 软件调试
    【香橙派AIPro+opencv】基础数据结构、颜色转换函数和颜色空间
    axios接口请求超时,重试方法
    源码编译Kdenlive视频编辑器
  • 原文地址:https://www.cnblogs.com/zhaorongbiao/p/15998940.html