likes
comments
collection
share

SpringBoot多租户业务的多数据源动态切换解决方案

作者站长头像
站长
· 阅读数 8

数据源切换方法

Springboot提供了AbstractRoutingDataSource抽象类,类名意思是数据源路由,让用户可以选择根据需要切换当前数据源 该类提供了一个抽象方法determineCurrentLookupKey(), 切换数据源时springboot会调用这个方法,所以只需要实现该方法,在该方法中返回需要切换的数据源名称即可

源码解读

1.从类关系图中可以看出AbstractRoutingDataSource类实现的是DataSource接口(非最底层), 其要求实现一个方法getConnection(),即获取DB连接

SpringBoot多租户业务的多数据源动态切换解决方案

2.AbstractRoutingDataSource实现了这两个方法

SpringBoot多租户业务的多数据源动态切换解决方案

其中determineTargetDataSource()调用determineCurrentLookupKey()方法,取到当前设定的查找键,通过查找键在上下文this.resolvedDataSources属性中尝试获取DataSource对象,这个对象即当前连接的数据源

SpringBoot多租户业务的多数据源动态切换解决方案

3.那么this.resolvedDataSources在哪里维护? AbstractRoutingDataSource类实现了InitializingBean类的afterPropertiesSet()方法, 在bean的所有属性设置完成后变会调用此方法,可以看到this.resolvedDataSources从this.targetDataSources取的信息;

SpringBoot多租户业务的多数据源动态切换解决方案

所以只需要改变this.targetDataSources,并且触发afterPropertiesSet(),即可改变this.resolvedDataSources;后续改变determineCurrentLookupKey()的返回值(key),在调用getConnection()时即可获取到指定的数据源

多租户业务背景

多租户业务场景下,往往每个租户都独立一个数据库(是否独立数据源实例根据实际需要处理),每个租户的数据在数据库层面先做了隔离,在开展详细业务编写时就可以不用考虑不同租户的数据会混淆。但是随之而来的就是数据源灵活切换的需求,需要封装一套方法,在业务编写时可以根据提供的租户代码便捷的切换到对应的数据源

提供的切换方式

1.注解方式切换 提供一个注解,可以根据租户代码切换,也可以根据配置文件中写定的数据源名称切换

2.直接调用方法方式切换 提供一个租户rds切换类,在编写业务代码时调用方法切换,该方式可以让租户代码以变量形式传递,无需提前知道

实现步骤概要

1.添加pom依赖、配置数据源信息 2.编写数据源配置类,将数据源配置信息注入到容器 3.编写DynamicDataSource类继承AbstractRoutingDataSource抽象类,维护当前数据源信息,提供切换方法 4.编写租户rds切换类,业务切换数据源时统一调用此类 5.编写自定义注解 6.编写切面类,将连接点直接设定在编写的自定义注解上,根据参数等调用rds切换类切换数据源 7.异常类、异常枚举类,规范异常抛出

详细步骤

1.pom依赖添加、配置数据源信息

pom.xml

<dependencies>
	<!-- mysql ps:由于连接的数据库是5.6所以用较老的包,读者可以根据数据库版本选择 -->
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<version>5.1.38</version>
		<scope>runtime</scope>
	</dependency>
	<!-- aop -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-aop</artifactId>
	</dependency>
	<!-- druid数据源 -->
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>druid-spring-boot-starter</artifactId>
		<version>1.1.10</version>
	</dependency>
</dependencies>

application.yml

# 主配置
spring:
  # 数据源配置
  datasource:
    # 修改数据源为druid
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver #这个要根据mysql-connector-java版本
    # druid配置
    druid:
      # 主数据源
      master:
        driver-class-name: com.mysql.jdbc.Driver
        # 默认数据库连接(配置库)
        url: jdbc:mysql://xxx:xxx/config?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: xxx
        password: xxx
      # 递增db配置
      db1:
        driver-class-name: com.mysql.jdbc.Driver #这个要根据mysql-connector-java版本
        url: jdbc:mysql://xxx:xxx/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: xxx
        password: xxx
      initial-size: 5 # 初始化时建立物理连接的个数
      max-active: 30 # 最大连接池数量
      min-idle: 5 # 最小连接池数量
      max-wait: 60000 # 获取连接时最大等待时间,单位毫秒
      time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      min-evictable-idle-time-millis: 300000 # 连接保持空闲而不被驱逐的最小时间
      validation-query: SELECT 1 FROM DUAL # 用来检测连接是否有效的sql,要求是一个查询语句
      test-while-idle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      test-on-borrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      test-on-return: false # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      pool-prepared-statements: true # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
      max-pool-prepared-statement-per-connection-size: 50 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
      filters: stat,wall,log4j2 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计;配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      use-global-data-source-stat: true # 合并多个DruidDataSource的监控数据
      stat-view-servlet:
        allow: '' # IP白名单(没有配置或者为空,则允许所有访问) allow: 127.0.0.1,192.168.163.1
        deny: '' # IP黑名单 (存在共同时,deny优先于allow)
        login-password: xxxxxx # 登录密码
        login-username: admin # 登录名
        reset-enable: false #  禁用HTML页面上的“Reset All”功能
        url-pattern: /druid/* # 配置DruidStatViewServlet
      web-stat-filter: # 配置DruidStatFilter
        enabled: true
        exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'
        url-pattern: /*

1.配置里面包含了一些druid的配置,可以根据业务需要自行配置 2.其中,spring.datasource.druid.master为主数据源,也是配置库数据源,租户库数据源连接信息会在配置库中获取,spring.datasource.druid.db1为递增数据源,db1可以命名为具体的业务库名称,这里仅仅方便理解取名为db1


2.编写数据源配置类,将数据源配置信息注入到容器

数据源配置类DataSourceConfig

@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) // 排除 DataSourceAutoConfiguration 的自动配置,避免环形调用
public class DataSourceConfig {
    /**
     * 默认数据源
     *
     * @return
     */
    @Bean(DataSourceConstant.DATA_SOURCE_MASTER)
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource dataSourceMaster() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 递增数据源
     *
     * @return
     */
    @Bean(DataSourceConstant.DATA_SOURCE_DB_1)
    @ConfigurationProperties("spring.datasource.druid.db1")
    public DataSource dataSourceDb1() {
        return DruidDataSourceBuilder.create().build();
    }


    /**
     * 设置动态数据源为主数据源
     *
     * @return
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource() {
        // 将数据源设置进map
        DynamicDataSource.setDataSourceMap(DataSourceConstant.DATA_SOURCE_MASTER, dataSourceMaster());
        DynamicDataSource.setDataSourceMap(DataSourceConstant.DATA_SOURCE_DB_1, dataSourceDb1());
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 使用 Map 保存多个数据源,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上
        dynamicDataSource.setTargetDataSources(DynamicDataSource.dataSourceMap);
        return dynamicDataSource;
    }
}

数据源常量类

public class DataSourceConstant {
    private DataSourceConstant() {
    }

    /**
     * 这里的命名统一在配置文件命名的基础上加dataSource前缀且改小驼峰
     * 默认数据源名称
     */
    public static final String DATA_SOURCE_MASTER = "dataSourceMaster";

    /**
     * 递增可配数据源名称
     * 这里的命名统一在配置文件命名的基础上加dataSource前缀且改小驼峰
     * 后面可接着 db2... dbn 也可以根据
     */
    public static final String DATA_SOURCE_DB_1 = "dataSourceDb1";
}

此处先往DynamicDataSource.dataSourceMap将两个配置好的数据源连接信息写入,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上


3.编写DynamicDataSource类继承AbstractRoutingDataSource抽象类,维护当前数据源信息,提供切换方法

动态数据源类DynamicDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 存储当前线程的数据源key
     */
    private static final ThreadLocal<String> DATA_SOURCE_KEY = ThreadLocal.withInitial(() -> DataSourceConstant.DATA_SOURCE_MASTER);

    /**
     * 数据源map
     */
    public static Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>(1000);

    /**
     * 获取数据源key
     *
     * @return
     */
    public static String getDataSourceKey() {
        return DynamicDataSource.DATA_SOURCE_KEY.get();
    }

    /**
     * 设置数据源key
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        DynamicDataSource.DATA_SOURCE_KEY.set(key);
    }

    /**
     * 移除默认数据源key
     */
    public static void remove() {
        DynamicDataSource.DATA_SOURCE_KEY.remove();
    }

    /**
     * 切换成默认的数据源
     */
    public static void setDataSourceDefault() {
        setDataSource(DataSourceConstant.DATA_SOURCE_MASTER);
    }

    /**
     * 切换成指定数据源 前提是dataSourceMap中有该key
     * 外层调用时需要判断下map是否有,可靠性交给外层维护
     *
     * @param dataSource
     */
    public static void setDataSource(String dataSource) {
        setDataSourceKey(dataSource);
        // InitializingBean.afterPropertiesSet()是,实例化后,bean的所有属性初始化后调用;但是如果该bean是直接从容器中拿的,并不需要实例化动作
        // 这里直接拿到dataSource,手动触发一下,让AbstractRoutingDataSource.resolvedDataSources重新赋值,取到本类维护的map的值
        DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtil.getBean("dataSource");
        dynamicDataSource.afterPropertiesSet();
    }

    /**
     * 获取租户数据源配置
     *
     * @param tenantCode
     * @return
     */
    public static Object getDataSourceMap(String tenantCode) {
        return DynamicDataSource.dataSourceMap.get(tenantCode);
    }

    /**
     * 设置map
     *
     * @param dataSourceName
     * @return void
     * @author Linzs
     * @date 2021/8/28 11:53
     **/
    public static void setDataSourceMap(String dataSourceName, Object dataSource) {
        dataSourceMap.put(dataSourceName, dataSource);
    }

    /**
     * 设置map
     *
     * @param dataSourceName
     * @return void
     * @author Linzs
     * @date 2021/8/28 11:53
     **/
    public static void setDataSourceMap(String dataSourceName) {
        dataSourceMap.put(dataSourceName, SpringContextUtil.getBean(dataSourceName));
    }

    /**
     * 设置租户数据源配置
     *
     * @param rdsConfig
     * @return
     */
    public static void setDataSourceMap(RdsConfig rdsConfig) {
        DynamicDataSource.dataSourceMap.put(rdsConfig.getTenantCode(), getDruidDataSource(rdsConfig));
    }

    /**
     * 获取DruidDataSource
     *
     * @param rdsConfig
     * @return
     */
    private static DruidDataSource getDruidDataSource(RdsConfig rdsConfig) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl("jdbc:mysql://" + rdsConfig.getDbUrl() + ":" + rdsConfig.getDbPort() + "/" + rdsConfig.getDbName() + "?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=true&autoReconnect=true&serverTimezone=Asia/Shanghai");
        druidDataSource.setUsername(rdsConfig.getDbAccount());
        druidDataSource.setPassword(rdsConfig.getDbPassword());
        return druidDataSource;
    }

    /**
     * 重写determineCurrentLookupKey方法
     *
     * @return java.lang.Object
     * @date 2021/8/28 12:14
     **/
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSourceKey();
    }
}

  1. 维护了一个key,用于表示当前用的是哪个数据源
  2. 维护了一个map,用于springboot获取数据源信息
  3. 重写determineCurrentLookupKey方法,可参照上面源码解读理解

4.编写租户rds切换类,业务切换数据源时统一调用此类

RdsConfig类,该javabean描述rds的连接信息

@Data
public class RdsConfig implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 租户编码
     */
    private String tenantCode;

    /**
     * 数据库URL
     */
    private String dbUrl;

    /**
     * 数据库端口
     */
    private String dbPort;

    /**
     * 数据库名称
     */
    private String dbName;

    /**
     * 数据库账号
     */
    private String dbAccount;

    /**
     * 数据库密码
     */
    private String dbPassword;
}

具体rds切换服务类:TenantRdsServiceImpl类,实现TenantRdsService接口,这里不贴出来了

@Service
@Slf4j
public class TenantRdsServiceImpl implements TenantRdsService {
    @Autowired
    private TenantMapper tenantMapper;

    @Autowired
    private RdsMapper rdsMapper;

    /**
     * 获取rds配置
     *
     * @param tenantCode
     * @date 2021/8/28 13:53
     **/
    @Override
    public RdsConfig getRdsConfig(String tenantCode) {
        // 根据租户代码取租户表
        Tenant tenant = tenantMapper.selectByTenantCode(tenantCode);
        if (null == tenant) {
            return null;
        }
        // 取rds表
        Rds rds = rdsMapper.selectByPrimaryKey(tenant.getRdsId());
        if (null == rds) {
            return null;
        }
        // 转换为rds配置
        RdsConfig rdsConfig = new RdsConfig();
        rdsConfig.setDbUrl(rds.getHost());
        rdsConfig.setTenantCode(tenantCode);
        rdsConfig.setDbName(tenant.getDbName());
        rdsConfig.setDbAccount(rds.getAccount());
        rdsConfig.setDbPassword(rds.getPwd());
        rdsConfig.setDbPort(String.valueOf(rds.getPort()));
        return rdsConfig;
    }

    /**
     * 根据租户代码切换rds连接,同一个线程内rds配置只会查一次
     *
     * @param tenantCode
     * @date 2021/8/28 13:16
     **/
    @Override
    public void switchRds(String tenantCode) {
        if (StringUtils.isBlank(tenantCode)) {
            throw new TenantCodeIsBlankException();
        }
        // 如果当前已是这个租户rds则直接返回
        if (tenantCode.equals(DynamicDataSource.getDataSourceKey())) {
            return;
        }
        // 如果本地已有则不查了 改rds需要重启服务
        if (null == DynamicDataSource.getDataSourceMap(tenantCode)) {
            // 如果当前不是配置库则先切回配置库
            if (!DataSourceConstant.DATA_SOURCE_MASTER.equals(DynamicDataSource.getDataSourceKey())) {
                DynamicDataSource.setDataSourceDefault();
            }
            // 获取rds配置
            RdsConfig rdsConfig = getRdsConfig(tenantCode);
            if (null == rdsConfig) {
                throw new RdsNotFoundException();
            }
            DynamicDataSource.setDataSourceMap(rdsConfig);
        }
        // 切换到业务库
        DynamicDataSource.setDataSource(tenantCode);
    }

    /**
     * 根据数据源名称切换rds连接,同一个线程内rds配置只会查一次
     *
     * @param dataSourceName
     * @date 2021/8/28 13:16
     **/
    @Override
    public void switchRdsByDataSourceName(String dataSourceName) {
        if (StringUtils.isBlank(dataSourceName)) {
            throw new DataSourceNameIsEmptyException();
        }
        // 如果当前已是这个数据源直接返回
        if (dataSourceName.equals(DynamicDataSource.getDataSourceKey())) {
            return;
        }
        // 如果本地已有则不查了 改rds需要重启服务
        if (null == DynamicDataSource.getDataSourceMap(dataSourceName)) {
            throw new DataSourceNotExistException();
        }
        // 切换
        DynamicDataSource.setDataSource(dataSourceName);
    }
}

1.这里用到了两张表,一张是租户表(tenant)用于存储租户代码与rds的对应关系,另一张是DB连接信息(rds)表,用于存储数据源连接信息,具体的mapper和javabean的代码这里就不贴出来了,根据需求建表具体实现即可 2.提供了三个方法分别是根据租户代码获取rds连接信息,根据租户代码切换rds,根据数据源名称切换rds,切换方法中对当前连接信息做了判断,不会重复切换,也不会重复查配置库获取rds信息


5.编写自定义注解

自定义注解 @SwitchMasterRds

/**
 * 切换至主数据源-自定义注解
 * 这个仅为了方便使用,用SwitchRds注解指定为默认数据源也可以实现
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchMasterRds {
}

自定义注解 @SwitchRds

/**
 * 切换数据源-自定义注解
 */
// 注解作用目标;ElementType.METHOD表示该注解会用在方法上;ElementType.TYPE表示该注解会用在类,接口,枚举;
@Target({ElementType.METHOD, ElementType.TYPE})
// 注解策略属性;RetentionPolicy.RUNTIME表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchRds {
    /**
     * 根据数据源bean切换数据源
     * 此处可以切换的数据源在 DataSourceConfig 配置类中
     * 同时指定了tenantCode则这个优先
     */
    String dataSource() default "";

    /**
     * 动态切换-根据租户代码切换数据源
     */
    String tenantCode() default "";
}

1.SwitchRds注解既可以用租户代码切换rds,又可以使用数据源名称切换 2.SwitchMasterRds注解是为了方便切换成主数据源而添加的


6.编写切面类

SwitchMasterRds注解的切面类SwitchMasterRdsAspect

@Aspect
@Component
@Slf4j
public class SwitchMasterRdsAspect {
    /**
     * 租户rds服务类
     */
    @Autowired
    private TenantRdsService tenantRdsServiceImpl;

    /**
     * 切点
     * 连接点:直接指定为注解
     * 注意:com.xxx.SwitchMasterRds这里包名自行修改
     * @date 2021/8/27 14:26
     **/
    @Pointcut("@annotation(com.xxx.SwitchMasterRds)")
    public void myPointcut() {
    }

    /**
     * 环绕通知
     *
     * @return java.lang.Object
     * @date 2021/8/27 14:26
     **/
    @Around(value = "myPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object proceed;
        try {
            tenantRdsServiceImpl.switchRdsByDataSourceName(DataSourceConstant.DATA_SOURCE_MASTER);
            // 执行
            proceed = pjp.proceed();
        } finally {
            // todo 这里需要做移除切换的数据源也可以,但是如果没移除再下次切换的时候会先切换到配置库
        }
        return proceed;
    }
}

SwitchRds注解的切面类SwitchRdsAspect

@Aspect
@Component
@Slf4j
public class SwitchRdsAspect {
    /**
     * 租户rds服务类
     */
    @Autowired
    private TenantRdsService tenantRdsServiceImpl;

    /**
     * 切点
     * 连接点:直接指定为注解
     * 注意:com.xxx.SwitchRds这里包名自行修改
     * @date 2021/8/27 14:26
     **/
    @Pointcut("@annotation(com.xxx.SwitchRds)")
    public void myPointcut() {
    }

    /**
     * 环绕通知
     *
     * @return java.lang.Object
     * @date 2021/8/27 14:26
     **/
    @Around(value = "myPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        SwitchRds annotation = getAnnotation(pjp);
        // 获取注解上的租户代码
        String tenantCode = annotation.tenantCode();
        String dataSource = annotation.dataSource();
        Object proceed;
        try {
            if (StringUtils.isNotBlank(dataSource)) {
                tenantRdsServiceImpl.switchRdsByDataSourceName(dataSource);
            } else if (StringUtils.isNotBlank(tenantCode)) {
                tenantRdsServiceImpl.switchRds(tenantCode);
            } else {
                throw new DataSourceSwitchFailException();
            }
            // 执行
            proceed = pjp.proceed();
        } finally {
            // todo 这里需要做移除切换的数据源也可以,但是如果没移除再下次切换的时候会先切换到配置库
        }
        return proceed;
    }

    /**
     * 获取注解
     *
     * @param pjp
     * @date 2021/8/27 17:58
     **/
    private SwitchRds getAnnotation(ProceedingJoinPoint pjp) {
        // 尝试获取类上的注解
        SwitchRds annotation = pjp.getTarget().getClass().getAnnotation(SwitchRds.class);
        // 如果类上没有注解则获取方法上面的
        if (null == annotation) {
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            annotation = methodSignature.getMethod().getAnnotation(SwitchRds.class);
        }
        return annotation;
    }

}

这里将连接点直接设定在编写的自定义注解上,根据参数等调用rds切换类切换数据源


7.异常类、异常枚举类

ErrorInfo接口,规范异常枚举类

public interface ErrorInfo {
    /**
     * 异常码
     * @return int
     */
    int code();

    /**
     * 异常描述
     * @return String
     */
    String message();
}

处理异常枚举类,将所有错误类型以及错误代码枚举出来

/**
 * 处理异常枚举类
 */
public enum HandleExceptionEnum implements ErrorInfo {
    /**
     * 待处理
     */
    WAIT(0, "待处理"),

    /**
     * 成功
     */
    SUCCESS(10, "SUCCESS"),

    /**
     * 程序错误
     */
    ERROR(100, "程序错误"),


    /**
     * 公共 - rds配置未取到
     */
    C_GENERATE_RDS_NOT_FOUND(1001, "rds配置未取到"),

    /**
     * 公共 - 租户代码为空
     */
    C_GENERATE_TENANT_CODE_IS_BLANK(1002, "租户代码为空"),

    /**
     * 公共 - 数据源配置不存在
     */
    C_GENERATE_DATA_SOURCE_NOT_EXIST(1003, "数据源配置不存在"),

    /**
     * 公共 - 数据源名称为空
     */
    C_GENERATE_DATA_SOURCE_NAME_IS_EMPTY(1004, "数据源名称为空"),

    /**
     * 公共 - 数据源名称为空
     */
    C_GENERATE_DATA_SOURCE_SWITCH_FAIL(1005, "数据源切换失败"),


    // ------------------------------------------------------------------

    ;

    /**
     * 编码
     */
    private final int code;

    /**
     * 信息
     */
    private final String message;

    HandleExceptionEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public int code() {
        return code;
    }

    @Override
    public String message() {
        return message;
    }

    /**
     * code转换成enum
     *
     * @param code 错误码
     * @return HandleExceptionEnum
     */
    public static HandleExceptionEnum codeOf(int code) {
        for (HandleExceptionEnum item : HandleExceptionEnum.values()) {
            if (item.code() == code) {
                return item;
            }
        }
        return null;
    }

    /**
     * 指定code是否在枚举之内
     *
     * @param code 错误码
     * @return boolean
     */
    public static boolean contain(int code) {
        for (HandleExceptionEnum item : HandleExceptionEnum.values()) {
            if (item.code() == code) {
                return true;
            }
        }
        return false;
    }
}

处理异常基类,所有的处理异常全部继承该类,其保存ErrorInfo信息,可以方便获取错误代码等

/**
 * HandlerException
 */
public class HandlerException extends RuntimeException {
    /**
     * 异常信息
     */
    private final ErrorInfo errorInfo;

    /**
     * 无参构造方法默认为程序错误
     */
    public HandlerException() {
        super(HandleExceptionEnum.ERROR.message());
        this.errorInfo = HandleExceptionEnum.ERROR;
    }

    public HandlerException(HandleExceptionEnum handleExceptionEnum) {
        super(handleExceptionEnum.message());
        this.errorInfo = handleExceptionEnum;
    }

    public HandlerException(HandleExceptionEnum handleExceptionEnum, String message) {
        super(message);
        this.errorInfo = handleExceptionEnum;
    }

    /**
     * 根据异常类型获取code
     *
     * @param e
     * @return int
     */
    public static int getCode(Exception e){
        return e instanceof HandlerException ? ((HandlerException) e).getErrorInfo().code() : HandleExceptionEnum.ERROR.code();
    }

    /**
     * 获取异常信息
     *
     * @return ErrorInfo
     */
    public ErrorInfo getErrorInfo() {
        return errorInfo;
    }
}

具体的异常类,直接继承处理异常基类,文中主动抛出的异常全是这样方式编写,这里就不一一列举了

/**
 * rds配置未取到
 */
public class RdsNotFoundException extends HandlerException {
    public RdsNotFoundException() {
        super(HandleExceptionEnum.C_GENERATE_RDS_NOT_FOUND);
    }
}

使用

1.注解方式

@RestController
public class HelloController {
    /**
     * 切换到主数据源方式1
     */
    @GetMapping("/masterFirst")
    @SwitchRds(dataSource = DataSourceConstant.DATA_SOURCE_MASTER)
    public Object masterFirst() {
		// todo
    }

    /**
     * 切换到主数据源方式2
     */
    @GetMapping("/masterSecond")
    @SwitchMasterRds
    public Object masterSecond() {
		// todo
    }

    /**
     * 切换到其他已配置的数据源
     */
    @GetMapping("/other")
    @SwitchRds(dataSource = DataSourceConstant.DATA_SOURCE_DB_1)
    public Object other() {
		// todo
    }
	
    /**
     * 根据租户代码切换
     */
    @GetMapping("/tenant")
    @SwitchRds(tenantCode = "tenantxxx")
    public Object tenant() {
		// todo
    }
}

2.业务代码使用方式

try {
    tenantRdsServiceImpl.switchRds("any tenant code");
} catch (Exception e) {
    log.error("切换租户rds时出错:{},context:{}", e.getMessage(), context, e);
}