概述

1. 需求:

多数据源切换的情况,大多数是主、从数据库切换,而主从数据库多数情况下都是同种类的数据库。但是,实际工作中,也有可能需要多种类的数据库之间的切换。本文便是实现多种类数据库的切换。

2. 实际场景:

例如,本人实际工作中,需要将原有的postgresql(简称pg)数据库中的数据读取出来,根据某些特定规则,进行展示,但是该pg数据库,不在公司,不允许修改。而且就是展示数据,需要小,也不需要修改数据库。

 

一、基本配置

1. 在pom.xml中添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.apollo</groupId>
    <artifactId>datasource-switcher</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
    </parent>

    <dependencies>
        <!--web应用基本环境,如mvc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--切面相关-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--postgresql的驱动-->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

        <!--mysql的驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--mybatis-plus依赖于spring-boot-starter-jdbc,会将后者自动添加-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.3.3</version>
        </dependency>
    </dependencies>
</project>

2. application.yml配置

server:
  port: 11011
  servlet:
    context-path: /api/v1

spring:
  datasource:
    #postgresql数据库
    pg:
      jdbc-url: jdbc:postgresql://127.0.0.1:5432/switcher_pg
      driver-class-name: org.postgresql.Driver
      type: com.zaxxer.hikari.HikariDataSource
      username: postgres
      password: 123456
    #mysql数据库
    mysql:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/switcher_mysql?useUnicode=true&characterEncoding=utf8&useSSL=false
      #这里是com.*,而pg是org.*
      driver-class-name: com.mysql.jdbc.Driver
      type: com.zaxxer.hikari.HikariDataSource
      username: root
      password: 123456

mybatis:
  # 如果是放在src/main/java目录下 classpath:/package-name/*/mapper/*Mapper.xml
  # 如果是放在resource目录 classpath:/mapper/*Mapper.xml
  type-aliases-package: com.apollo.entity
  # 第二个“:”后边没有空格
  mapper-locations: classpath*:mapper/*.xml

logging:
  level:
    root: DEBUG

二、 配置代码及说明

1. Configuration文件

@Configuration
public class MultiDataSourceConfig {

    //实体类位置
    @Value("${mybatis.type-aliases-package}")
    private String typeAliasesPackage;

    //mapper的位置
    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;

    /**
     * postgresql数据源
     */
    @Bean(name = "pgDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.pg")
    public DataSource pgDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * mysql数据源
     */
    @Bean(name = "mysqlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 多数据源动态切换
     */
    @Bean
    public DataSource multiDataSource(
            @Qualifier("mysqlDataSource") DataSource mysqlDataSource) {
        Map<Object, Object> target = new HashMap<>();
        target.put(DataSourceEnum.PG, pgDataSource());
        target.put(DataSourceEnum.MYSQL, mysqlDataSource);

        AbstractRoutingDataSource dataSource = new DynamicDataSource();
        dataSource.setDefaultTargetDataSource(mysqlDataSource);
        dataSource.setTargetDataSources(target);
        return dataSource;
    }

    /**
     * mybatis-plus分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(
            PaginationInterceptor paginationInterceptor,
            @Qualifier("multiDataSource") DataSource multiDataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(multiDataSource);
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sqlSessionFactory.setConfiguration(configuration);
        sqlSessionFactory.setPlugins(new Interceptor[]{
                paginationInterceptor           //添加分页功能
        });
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactory.setMapperLocations(resolver.getResources(mapperLocations));  //配置mapper位置
        sqlSessionFactory.setGlobalConfig(globalConfiguration());
        sqlSessionFactory.setTypeAliasesPackage(typeAliasesPackage);//配置实体类位置
        return sqlSessionFactory.getObject();

    }

    /**
     * mybatis-plus中SQL执行效率插件,生产环境可以关闭
     */
    @Bean
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }

    @Bean
    public GlobalConfiguration globalConfiguration() {
        GlobalConfiguration conf = new GlobalConfiguration(new LogicSqlInjector());
        conf.setLogicDeleteValue("-1");
        conf.setLogicNotDeleteValue("1");
        conf.setIdType(0);
        conf.setMetaObjectHandler(new MyMetaObjectHandler());
        conf.setDbColumnUnderline(true);
        conf.setRefresh(true);
        return conf;
    }
}

说明:

1)multiDataSource方法中,名为target的Map的键值是DataSourceEnum类型,而不是字符串。

2. 数据源类型 DataSourceEnum

该enum用于确定数据源的类型,PG表示Postgresql数据库,MYSQL表示Mysql数据库。

public enum DataSourceEnum {
    PG("PG"), MYSQL("MYSQL");

    private String value;

    DataSourceEnum(String value) {
        this.value = value;
    }

    public String getValue() {
        return this.value;
    }
}

3. 获取数据源标识回调函数类 DynamicDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DsContextHolder.getDataSourceType();
    }
}

4. 自定义数据源切换注解 DsSwitcher

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface DsSwitcher {
    //默认数据源是mysql
    DataSourceEnum value() default DataSourceEnum.MYSQL;
}

说明:

1)注解的默认值是DataSourceEnum.MYSQL,与MultiDataSourceConfig类中multiDataSource实例的setDefaultTargetDataSource方法设置的默认值应该是相同的。

5. 切面类 DsSwitcherAspect

@Component
@Aspect
public class DsSwitcherAspect {

    @Pointcut("execution(* com.apollo.service..*.*(..))")
    private void serviceMethod() {}

    /**
     * 在调用前切换数据源
     */
    @Before("serviceMethod()")
    public void transServiceMethod(JoinPoint joinPoint) {
        switchDataSource(joinPoint);
    }

    /**
     * 在调用后(包括有异常的情况下),清空数据源标识
     */
    @After("serviceMethod()")
    public void clearDs(JoinPoint joinPoint) {
        clearDataSource(joinPoint);
    }

    /**
     * 根据注解改变数据源
     */
    private void switchDataSource(JoinPoint joinPoint) {
        MethodSignature signature =
                (MethodSignature) joinPoint.getSignature();
        DsSwitcher dsSwitcher =
                signature.getMethod().getAnnotation(DsSwitcher.class);
        if (!Objects.isNull(dsSwitcher) && !Objects.isNull(dsSwitcher.value())) {
            DataSourceEnum annoEnum = dsSwitcher.value();
            if (DataSourceEnum.PG == annoEnum) {
                DsContextHolder.setDataSourceType(DataSourceEnum.PG);
            } else if (DataSourceEnum.MYSQL == annoEnum) {
                DsContextHolder.setDataSourceType(DataSourceEnum.MYSQL);
            }
        }
    }

    /**
     * 在每次调用之后,清空数据源
     */
    private void clearDataSource(JoinPoint joinPoint) {
        MethodSignature signature =
                (MethodSignature) joinPoint.getSignature();
        DsSwitcher dsSwitcher =
                signature.getMethod().getAnnotation(DsSwitcher.class);
        if (!Objects.isNull(dsSwitcher) && !Objects.isNull(dsSwitcher.value())) {
            DsContextHolder.clearDataSourceType();
        }
    }
}

6. 数据表示存储类 DsContextHolder

public class DsContextHolder {
    private static final ThreadLocal<DataSourceEnum> contextHolder = new ThreadLocal<>();

    private DsContextHolder() throws Exception {
        throw new InstantiationException("无法实例化");
    }

    /**
     * 设置数据源标识
     */
    public static void setDataSourceType(DataSourceEnum dsEnum) {
        contextHolder.set(dsEnum);
    }

    /**
     * 获取当前数据源的标识
     */
    public static DataSourceEnum getDataSourceType() {
        return contextHolder.get();
    }

    /**
     * 清空数据源标识
     */
    public static void clearDataSourceType() {
        contextHolder.remove();
    }
}

说明:

1)ThreadLocal中泛型是DataSourceEnum,而不是String。

2)在AbstractRoutingDataSource的determineTargetDataSource方法中,会通过:

/**
* 重新获取目标数据源
*/
protected DataSource determineTargetDataSource() {
	Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
	Object lookupKey = determineCurrentLookupKey();//获取目标数据源键值
	DataSource dataSource = this.resolvedDataSources.get(lookupKey);//根据键值,获取数据源
	if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
		dataSource = this.resolvedDefaultDataSource;//如果没有获取到,证明不存在,则获取默认数据源
	}
	if (dataSource == null) {
		throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
	}
	return dataSource;
}

来获取DataSource。这里的determineCurrentLookupKey的返回值,一定和ThreadLocal的泛型类型相同。也就是ThreadLocal<determineCurrentLookupKey返回值的类型>。否则第7行无法从Map中获取对应的DataSource。

3)在MultiDataSourceConfig中,配置多数据源multiDataSource时,

/**
     * 多数据源动态切换
     */
    @Bean
    public DataSource multiDataSource(
            @Qualifier("mysqlDataSource") DataSource mysqlDataSource) {
        Map<Object, Object> target = new HashMap<>();
        target.put(DataSourceEnum.PG, pgDataSource());
        target.put(DataSourceEnum.MYSQL, mysqlDataSource);

        AbstractRoutingDataSource dataSource = new DynamicDataSource();
        dataSource.setDefaultTargetDataSource(mysqlDataSource);
        dataSource.setTargetDataSources(target);
        return dataSource;
    }

注意这里的Map<Object, Object> target,他的键值类型,和ThreadLocal的泛型类型要一致。

总结,以下3个位置的类型要相同:

1)在MultiDataSourceConfig中,multiDataSource实例中,setTargetDataSources设置的map的键值;

2)AbstractRoutingDataSource中的determineTargetDataSource的返回值;

3)ThreadLocal的泛型类型。

7. 程序入口类

@SpringBootApplication
@MapperScan("com.apollo.dao")   //注意这里加上扫描地址
public class DsSwitchApplication {
    public static void main(String[] args) {
        SpringApplication.run(DsSwitchApplication.class, args);
    }
}

三、使用数据源切换注解

1. Postgresql数据库对应的Service实现类:

@Service
public class WeekServiceImpl implements WeekService {

    @Autowired
    private WeekDao weekDao;

    @DsSwitcher(DataSourceEnum.PG)    //这里的注解是必须加的
    @Override
    public List<Week> getAllWork() {
        return weekDao.selectAll();
    }
}

2. Mysql数据库对应的Service实现类

@Service
public class ProvinceServiceImpl implements ProvinceService {

    @Autowired
    private ProvinceDao provinceDao;

    @DsSwitcher(DataSourceEnum.MYSQL)//由于默认数据源是mysql,这里可以不用设置
    @Override
    public List<Province> getAllProvince() {
        return provinceDao.selectAll();
    }
}