Решение для динамического переключения с несколькими источниками данных для многопользовательского бизнеса SpringBoot

Spring Boot

Способ переключения источника данных

Springboot предоставляет абстрактный класс AbstractRoutingDataSource, имя класса означает маршрутизацию источника данных, что позволяет пользователям переключать текущий источник данных по мере необходимости.
Этот класс предоставляет абстрактный метод defineCurrentLookupKey(), который вызывается Springboot при переключении источников данных, поэтому вам нужно только реализовать этот метод и вернуть имя источника данных для переключения в этом методе.

Интерпретация исходного кода

1. Из диаграммы классов видно, что класс AbstractRoutingDataSource реализует интерфейс DataSource (не нижний слой), Требуется реализация метода getConnection(), то есть для получения соединения с БД

image.png

2.AbstractRoutingDataSource реализует эти два метода.

image.png

Среди них: defineTargetDataSource() вызывает метод defineCurrentLookupKey() для получения текущего установленного ключа поиска и пытается получить объект DataSource в контексте свойства this.resolvedDataSources с помощью ключа поиска, который является текущим подключенным источником данных.

image.png

3. Итак, где хранится this.resolvedDataSources? Класс AbstractRoutingDataSource реализует метод afterPropertiesSet() класса InitializingBean, После установки всех свойств бина будет вызван этот метод, и вы сможете увидеть информацию, полученную this.resolvedDataSources из this.targetDataSources;

image.png

Таким образом, вам нужно только изменить this.targetDataSources и запустить afterPropertiesSet(), чтобы изменить this.resolvedDataSources; затем изменить возвращаемое значение (ключ) defineCurrentLookupKey(), вы можете получить указанный источник данных при вызове 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. Конфигурация содержит некоторые конфигурации друидов, которые можно настроить в соответствии с потребностями бизнеса.
2. Среди них spring.datasource.druid.master является основным источником данных, а также источником данных библиотеки конфигурации.Информация о подключении источника данных библиотеки арендатора будет получена в библиотеке конфигурации. .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.


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. Ключ поддерживается, чтобы указать, какой источник данных используется в настоящее время.
  2. Для Springboot поддерживается карта для получения информации об источнике данных.
  3. Перепишите метод defineCurrentLookupKey, вы можете обратиться к приведенному выше исходному коду для интерпретации и понимания.

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, а другая — таблица информации о подключении к БД (rds), которая используется для хранения информации о подключении к источнику данных. код картографа и 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. Напишите класс аспекта

Класс аспекта SwitchMasterRdsAspect аннотации SwitchMasterRds

@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;
    }
}

Класс аспекта SwitchRdsAspect аннотации SwitchRds

@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);
}