多数据源切换的情况,大多数是主、从数据库切换,而主从数据库多数情况下都是同种类的数据库。但是,实际工作中,也有可能需要多种类的数据库之间的切换。本文便是实现多种类数据库的切换。
例如,本人实际工作中,需要将原有的postgresql(简称pg)数据库中的数据读取出来,根据某些特定规则,进行展示,但是该pg数据库,不在公司,不允许修改。而且就是展示数据,需要小,也不需要修改数据库。
<?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>
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
@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类型,而不是字符串。
该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;
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DsContextHolder.getDataSourceType();
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface DsSwitcher {
//默认数据源是mysql
DataSourceEnum value() default DataSourceEnum.MYSQL;
}
1)注解的默认值是DataSourceEnum.MYSQL,与MultiDataSourceConfig类中multiDataSource实例的setDefaultTargetDataSource方法设置的默认值应该是相同的。
@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();
}
}
}
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的泛型类型要一致。
1)在MultiDataSourceConfig中,multiDataSource实例中,setTargetDataSources设置的map的键值;
2)AbstractRoutingDataSource中的determineTargetDataSource的返回值;
3)ThreadLocal的泛型类型。
@SpringBootApplication
@MapperScan("com.apollo.dao") //注意这里加上扫描地址
public class DsSwitchApplication {
public static void main(String[] args) {
SpringApplication.run(DsSwitchApplication.class, args);
}
}
@Service
public class WeekServiceImpl implements WeekService {
@Autowired
private WeekDao weekDao;
@DsSwitcher(DataSourceEnum.PG) //这里的注解是必须加的
@Override
public List<Week> getAllWork() {
return weekDao.selectAll();
}
}
@Service
public class ProvinceServiceImpl implements ProvinceService {
@Autowired
private ProvinceDao provinceDao;
@DsSwitcher(DataSourceEnum.MYSQL)//由于默认数据源是mysql,这里可以不用设置
@Override
public List<Province> getAllProvince() {
return provinceDao.selectAll();
}
}