zoukankan      html  css  js  c++  java
  • SpringBoot多数据源

    前言

    多数据源的核心就是向 IOC 容器注入 AbstractRoutingDataSource 和如何切换数据源。注入的方式可以是注册 BeanDefinition 或者是构建好的 Bean,切换数据源的方式可以是方法参数或者是注解切换(其他的没想象出来),具体由需求决定。

    我的需求是统计多个库的数据,将结果写入另一个数据库,统计的数据库数量是不定的,无法通过 @Bean 直接注入,又是统计任务,DAO 层注解切换无法满足,因此选择注册(AbstractRoutingDataSource 的)BeanDefinition 和方法参数切换来实现。下面以统计统计中日韩用户到结果库为例。

    配置文件

    master 为结果库,其他为被统计的数据库(china、japan 可以用枚举唯一标识,当然也可以用 String):

    dynamic:
      dataSources:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/result?useUnicode=true&characterEncoding=utf8xxxxxxxx
          username: root
          password: 123456
        china:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/china?useUnicode=true&characterEncoding=utf8xxxxxxxx
          username: root
          password: 123456
        japan:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/japan?useUnicode=true&characterEncoding=utf8xxxxxxxx
          username: root
          password: 123456
        korea:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/korea?useUnicode=true&characterEncoding=utf8xxxxxxxx
          username: root
          password: 123456
    

    对应的配置类:

    package com.statistics.dynamicds.core.config;
    
    import com.statistics.dynamicds.core.Country;
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.Map;
    
    import static com.statistics.dynamicds.core.config.MultiDataSourceProperties.PREFIX;
    
    @Data
    @Configuration
    @ConfigurationProperties(prefix = PREFIX)
    public class MultiDataSourceProperties {
      public static final String PREFIX = "dynamic";
      private Map<Country, DataSourceProperties> dataSources;
    
      @Data
      public static class DataSourceProperties {
        private String driverClassName;
        private String url;
        private String username;
        private String password;
      }
    }
    
    package com.statistics.dynamicds.core;
    
    public enum Country {
      MASTER("master", 0),
    
      CHINA("china", 86),
      JAPAN("japan", 81),
      KOREA("korea", 82),
      // 其他国家省略
    
      private final String name;
      private final int id;
    
      Country(String name, int id) {
        this.name = name;
        this.id = id;
      }
    
      public int getId() {
        return id;
      }
    
      public String getName() {
        return name;
      }
    }
    

    依赖

    ORM 用的 JPA,SpringBoot 版本为 2.3.7.RELEASE,通过 Lombok 简化 GetSet。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <version>1.18.22</version>
         <scope>provided</scope>
    </dependency>
    

    构建 AbstractRoutingDataSource

    Spring 的动态数据源需要注入 AbstractRoutingDataSource,因为配置文件中被统计数据源不是固定的,所以不能通过 @Bean 注解注入,需要手动构建。

    要在启动类加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class)。

    要在启动类加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class)。

    要在启动类加上 @Import(MultiDataSourceImportBeanDefinitionRegistrar.class),重要的事情写三行。

    package com.statistics.dynamicds.autoconfig;
    
    import com.statistics.dynamicds.core.DynamicDataSourceRouter;
    import com.statistics.dynamicds.core.Country;
    import com.statistics.dynamicds.core.config.MultiDataSourceProperties;
    import com.zaxxer.hikari.HikariDataSource;
    import org.springframework.beans.factory.support.AbstractBeanDefinition;
    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.boot.context.properties.bind.Binder;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.EnvironmentAware;
    import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
    import org.springframework.core.env.Environment;
    import org.springframework.core.type.AnnotationMetadata;
    
    import javax.annotation.Nonnull;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import static com.statistics.dynamicds.core.config.MultiDataSourceProperties.PREFIX;
    
    public class MultiDataSourceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
      public static final String DATASOURCE_BEANNAME = "dynamicDataSourceRouter";
      private Environment environment;
    
      @Override
      public void registerBeanDefinitions(@Nonnull AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        MultiDataSourceProperties multiDataSourceProperties = Binder.get(environment)
                .bind(PREFIX, MultiDataSourceProperties.class)
                .orElseThrow(() -> new RuntimeException("no found dynamicds config"));
        final HikariDataSource[] defaultTargetDataSource = {null};
        Map<Country, HikariDataSource> targetDataSources = multiDataSourceProperties.getDataSources().entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> {
                  MultiDataSourceProperties.DataSourceProperties dataSourceProperties = entry.getValue();
                  HikariDataSource dataSource = DataSourceBuilder.create()
                          .type(HikariDataSource.class)
                          .driverClassName(dataSourceProperties.getDriverClassName())
                          .url(dataSourceProperties.getUrl())
                          .username(dataSourceProperties.getUsername())
                          .password(dataSourceProperties.getPassword())
                          .build();
                  dataSource.setPoolName("HikariPool-" + entry.getKey());
                  if (Country.MASTER == entry.getKey()) {
                    defaultTargetDataSource[0] = dataSource;
                  }
                  return dataSource;
                }));
        targetDataSources.remove(Country.MASTER);
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(DynamicDataSourceRouter.class)
                .addConstructorArgValue(defaultTargetDataSource[0])
                .addConstructorArgValue(targetDataSources)
                .getBeanDefinition();
        registry.registerBeanDefinition(DATASOURCE_BEANNAME, beanDefinition);
      }
    
      @Override
      public void setEnvironment(@Nonnull Environment environment) {
        this.environment = environment;
      }
    }
    
    

    上面代码中 MultiDataSourceProperties 不是由 @Resource 或者 @Autowired 获取的是因为 ImportBeanDefinitionRegistrar 执行的很早,此时 @ConfigurationProperties 的配置参数类还没有注入,因此要手动获取(加 @ConfigurationProperties 注解是为了使 IOC 容器中其他 Bean 能获取配置的 Country,以此来切换数据源)。

    下面是 AbstractRoutingDataSource 的实现类 DynamicDataSourceRouter:

    package com.statistics.dynamicds.core;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    import java.util.Map;
    
    public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
      public DynamicDataSourceRouter(Object defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        this.setDefaultTargetDataSource(defaultTargetDataSource);
        this.setTargetDataSources(targetDataSources);
      }
    
      @Override
      protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getLookupKey();
      }
    }
    

    数据源切换

    数据源的切换由 DataSourceContextHolder 和切面 DynamicDataSourceAspect 控制:

    package com.statistics.dynamicds.core;
    
    public class DataSourceContextHolder {
      private static final ThreadLocal<Country> HOLDER = ThreadLocal.withInitial(() -> Country.MASTER);
    
      public static void setLookupKey(Country lookUpKey) {
        HOLDER.set(lookUpKey);
      }
    
      public static Country getLookupKey() {
        return HOLDER.get();
      }
    
      public static void clear() {
        HOLDER.remove();
      }
    }
    
    package com.statistics.dynamicds.core;
    
    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.springframework.stereotype.Component;
    
    @Aspect
    @Component
    public class DynamicDataSourceAspect {
    
      @Pointcut("execution(* com.statistics.dao..*.*(..))")
      void aspect() {
    
      }
    
      @Around("aspect()")
      public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        for (Object arg : joinPoint.getArgs()) {
          if (arg instanceof Country) {
            DataSourceContextHolder.setLookupKey((Country) arg);
            break;
          }
        }
        try {
          return joinPoint.proceed();
        }finally {
          DataSourceContextHolder.clear();
        }
      }
    }
    

    事务

    当没有事务时可以随意切换数据源。如果有事务就比较麻烦了,必须在执行 @Transactional 注解修饰的方法前(事务开始前)完成数据源的切换,也就是必须通过 DataSourceContextHolder.setLookupKey() 手动切换数据源,且被 @Transactional 修饰的方法内不能有不同数据源的写(但可以用 @Transactional(propagation = Propagation.NOT_SUPPORTED) 修饰 DAO 方法完成其他数据源的读)。

    目录

    .
    └─com
        └─statistics
            │  StatisticsApplication.java
            │
            ├─dao
            │      UserDao.java
            │
            ├─dynamicds
            │  ├─autoconfig
            │  │      MultiDataSourceImportBeanDefinitionRegistrar.java
            │  │
            │  └─core
            │      │  DataSourceContextHolder.java
            │      │  DynamicDataSourceAspect.java
            │      │  DynamicDataSourceRouter.java
            │      │  Country.java
            │      │
            │      └─config
            │              MultiDataSourceProperties.java
    

    总结

    以上就完成了多数据源配置,使用时只需要按照在 dao 层的方法参数中加一个 Country 枚举就可以了。

    如果无法用枚举标识数据源也可以换成 String,关于这个数据源的其他信息在内部类 DataSourceProperties 加一个 map 即可,总之就是按照自己的需求扩展。

  • 相关阅读:
    大话设计模式笔记(十三)の状态模式
    大话设计模式笔记(十二)の抽象工厂模式
    大话设计模式笔记(十一)の观察者模式
    大话设计模式笔记(十)の建造者模式
    大话设计模式笔记(九)の外观模式
    大话设计模式笔记(八)の模板方法模式
    大话设计模式笔记(七)の原型模式
    Vue(十二):自定义指令和函数渲染
    Vue(十一):组件边界
    Vue(十):混入、插件和过滤器
  • 原文地址:https://www.cnblogs.com/hligy/p/15507306.html
Copyright © 2011-2022 走看看