SpringBoot多数据源配置的全过程记录

 更新时间:2021年11月05日 11:02:04   作者:装在瓶子里的西班牙阳光  
在用SpringBoot开发项目时,随着业务量的扩大,我们通常会进行数据库拆分或是引入其他数据库,从而我们需要配置多个数据源,下面这篇文章主要给大家介绍了关于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;
            }));
    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();
    }
  }
}

目录

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

总结

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

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

相关文章

  • 教你用Java实现RSA非对称加密算法

    教你用Java实现RSA非对称加密算法

    今天带各位小伙伴学习怎么用Java实现RSA非对称加密算法,文中有非常详细的解释及代码示例,对正在学java算法的小伙伴们很有帮助,需要的朋友可以参考下
    2021-05-05
  • Java如何使用jar命令打包

    Java如何使用jar命令打包

    把多个文件打包成一个压缩包——这个压缩包和WinZip的压缩格式是一样的,区别在于jar压缩的文件默认多一个META-INF的文件夹,该文件夹里包含一个MANIFEST.MF的文件,本文给大家介绍Java如何使用jar命令打包,感兴趣的朋友跟随小编一起看看吧
    2023-10-10
  • 解决使用mybatis取值,字段赋值错误的问题

    解决使用mybatis取值,字段赋值错误的问题

    这篇文章主要介绍了解决使用mybatis取值,字段赋值错误的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • Spring中ApplicationContextAware的使用方法详解

    Spring中ApplicationContextAware的使用方法详解

    ApplicationContextAware 通过它Spring容器会自动把上下文环境对象调用ApplicationContextAware接口中的setApplicationContext方法,这篇文章主要介绍了Spring中ApplicationContextAware的作用,需要的朋友可以参考下
    2023-03-03
  • IDEA2020.3.2版本自动注释类和方法注释模板配置步骤详解

    IDEA2020.3.2版本自动注释类和方法注释模板配置步骤详解

    这篇文章主要介绍了IDEA2020.3.2版本自动注释类和方法注释模板配置步骤,本文给大家分享了我自己创建过程通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • Spring boot中自定义Json参数解析器的方法

    Spring boot中自定义Json参数解析器的方法

    这篇文章主要介绍了Spring boot中自定义Json参数解析器的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • 教你使用springboot配置多数据源

    教你使用springboot配置多数据源

    发现有很多小伙伴还不会用springboot配置多数据源,今天特地给大家整理了本篇文章,文中有非常详细的图文介绍及代码示例,对正在学习java的小伙伴很有帮助,需要的朋友可以参考下
    2021-05-05
  • 浅谈Java几种文件读取方式耗时

    浅谈Java几种文件读取方式耗时

    本文主要介绍了浅谈Java几种文件读取方式耗时,主要介绍了4种,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • maven私有镜像仓库nexus部署使用

    maven私有镜像仓库nexus部署使用

    Nexus在企业开发中还是比较常用的私有仓库管理工具,本文主要介绍了maven私有镜像仓库nexus部署使用,具有一定的参考价值,感兴趣的可以了解一下
    2024-07-07
  • 详解springboot 使用c3p0数据库连接池的方法

    详解springboot 使用c3p0数据库连接池的方法

    本篇文章主要介绍了springboot 使用c3p0数据库连接池的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-09-09

最新评论