Spring Boot 多数据源动态切换
在使用SpringBoot日常开发中,难免会连接多个数据库,一般可以使用Mybatis配置多个SqlSessionFactoryBean实现不同的数据源扫描不同的mapper,本文将在SpringBoot中使用Mybatis + AbstractRoutingDataSource + Java注解实现多数据源动态切换。
注意:使用Spring原生声明式事务注解@Transactional将会使动态切换功能失效,并且原生事务管理器不支持管理多个数据源,如果要支持分布式事务,考虑使用atomikos或者bitronix。
一、pom.xml引入需要的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
二、数据源以及Mybatis配置
在这里有master和slave两个数据库,对应的mapper类分别在com.realjt.smart.boot.mapper.master和com.realjt.smart.boot.mapper.slave下,对应的mapper配置xml文件分别在lasspath:mybatis/master/目录和classpath:mybatis/slave目录。
master.datasource.type=com.zaxxer.hikari.HikariDataSource
master.datasource.url=jjdbc:mysql://192.168.1.102:3306/service?serverTimezone=Asia/Shanghai
master.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
master.datasource.username=AAA
master.datasource.password=XXX
master.datasource.hikari.connection-timeout=3000
master.datasource.hikari.connection-test-query=select 1 from dual
slave.datasource.type=com.zaxxer.hikari.HikariDataSource
slave.datasource.url=jdbc:mysql://192.168.1.102:3306/service?serverTimezone=Asia/Shanghai
slave.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
slave.datasource.username=BBB
slave.datasource.password=XXX
slave.datasource.hikari.connection-timeout=3000
slave.datasource.hikari.connection-test-query=select 1 from dual
mybatis-plus.mapper-locations=classpath:mybatis/master/*.xml,classpath:mybatis/slave/*.xml
mybatis-plus.configuration.jdbc-type-for-null=NULL
mybatis-plus.configuration.call-setters-on-nulls=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
mybatis-plus.global-config.banner=false
三、继承AbstractRoutingDataSource.java
package com.realjt.smart.boot.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import com.realjt.smart.boot.constant.DataSourceType;
public class DynamicDataSource extends AbstractRoutingDataSource
{
/**
* 用来记录当前线程使用的是哪个数据源
*/
private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey()
{
return CONTEXT_HOLDER.get();
}
public static void setDataSourceType(DataSourceType dataSourceType)
{
CONTEXT_HOLDER.set(dataSourceType);
}
public static void clearDataSourceType(DataSourceType dataSourceType)
{
CONTEXT_HOLDER.remove();
}
}
四、数据源枚举类与mapper所在位置绑定
package com.realjt.smart.boot.constant;
/**
* 数据库类型
*/
public enum DataSourceType
{
/**
* 主库
*/
MASTER("master", "com.realjt.smart.boot.mapper.master"),
/**
* 从库
*/
SLAVE("slave", "com.realjt.smart.boot.mapper.slave");
private String type;
private String packageName;
private DataSourceType(String type, String packageName)
{
this.type = type;
this.packageName = packageName;
}
public String getType()
{
return type;
}
public String getPackageName()
{
return packageName;
}
}
五、数据源选择器注解
package com.realjt.smart.boot.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.realjt.smart.boot.constant.DataSourceType;
/**
* 数据源选择注解
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceSelector
{
DataSourceType value();
}
六、Mybatis拦截器
用Mybatis拦截器在执行sql前获取Mapper接口上的自定义注解,实现动态切换数据源。
package com.realjt.smart.boot.interceptor;
import java.lang.reflect.Method;
import java.util.Properties;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import com.realjt.smart.boot.annotation.DataSourceSelector;
import com.realjt.smart.boot.constant.DataSourceType;
import com.realjt.smart.boot.datasource.DynamicDataSource;
@Intercepts({
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }),
@Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) })
public class DynamicDataSourceInterceptor implements Interceptor
{
@Override
public Object intercept(Invocation invocation) throws Throwable
{
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
DataSourceType dataSourceType = null;
// 包含了全类名和方法名
String namespace = mappedStatement.getId();
String className = namespace.substring(0, namespace.lastIndexOf("."));
String methedName = namespace.substring(namespace.lastIndexOf(".") + 1, namespace.length());
// 先获取类上的注解
Class<?> clazz = Class.forName(className);
DataSourceSelector classAnnotation = clazz.getAnnotation(DataSourceSelector.class);
if (null != classAnnotation)
{
dataSourceType = classAnnotation.value();
}
// 再获取方法上的注解
for (Method method : clazz.getMethods())
{
if (method.getName().equals(methedName))
{
DataSourceSelector methodAnnotation = method.getAnnotation(DataSourceSelector.class);
if (null != methodAnnotation)
{
dataSourceType = methodAnnotation.value();
}
break;
}
}
if (null == dataSourceType)
{
// 如果没有注解则根据所在包设置数据源
String packageName = clazz.getPackage().getName();
for (DataSourceType value : DataSourceType.values())
{
if (packageName.startsWith(value.getPackageName()))
{
dataSourceType = value;
break;
}
}
}
if (null != dataSourceType)
{
DynamicDataSource.setDataSourceType(dataSourceType);
}
// 执行目标方法
Object proceed = invocation.proceed();
return proceed;
}
@Override
public Object plugin(Object target)
{
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties)
{
}
}
七、把数据源注入到Spring中
注意排除掉DataSourceAutoConfiguration.class,在DynamicDataSource上声明@Primary注解,配置事务管理器
package com.realjt.smart.boot;
/**
* 启动主程序
*/
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@MapperScan("com.realjt.smart.boot.mapper")
@EnableScheduling
public class SmartBootApplication implements WebMvcConfigurer
{
public static void main(String[] args)
{
SpringApplication springApplication = new SpringApplication(SmartBootApplication.class);
springApplication.setBannerMode(Banner.Mode.OFF);
springApplication.run(args);
}
@Bean
@ConfigurationProperties(prefix = "master.datasource")
public DataSourceProperties masterDataSourceProperties()
{
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties(prefix = "master.datasource.hikari")
public HikariDataSource masterDataSource()
{
DataSourceProperties masterDataSourceProperties = masterDataSourceProperties();
HikariDataSource dataSource = masterDataSourceProperties.initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
if (StringUtils.hasText(masterDataSourceProperties.getName()))
{
dataSource.setPoolName(masterDataSourceProperties.getName());
}
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "slave.datasource")
public DataSourceProperties slaveDataSourceProperties()
{
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties(prefix = "slave.datasource.hikari")
public HikariDataSource slaveDataSource()
{
DataSourceProperties slaveDataSourceProperties = slaveDataSourceProperties();
HikariDataSource dataSource = slaveDataSourceProperties.initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
if (StringUtils.hasText(slaveDataSourceProperties.getName()))
{
dataSource.setPoolName(slaveDataSourceProperties.getName());
}
return dataSource;
}
@Bean
@Primary
public DataSource dynamicDataSource()
{
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
Map<Object, Object> targetDataSources = new HashMap<>(16);
targetDataSources.put(DataSourceType.MASTER, masterDataSource());
targetDataSources.put(DataSourceType.SLAVE, slaveDataSource());
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager transactionManager()
{
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(
dynamicDataSource());
return dataSourceTransactionManager;
}
}
八、把自定义Mybatis拦截器配置到SqlSessionFactory中
也可以用BeanPostProcessor为SqlSessionFactory添加自定义拦截器。
package com.realjt.smart.boot.datasource;
import java.util.List;
import javax.annotation.PostConstruct;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import com.realjt.smart.boot.interceptor.DynamicDataSourceInterceptor;
@ConditionalOnBean(SqlSessionFactory.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class DynamicDataSourceAutoConfiguration
{
@Autowired
private List<SqlSessionFactory> sqlSessionFactories;
@PostConstruct
public void addPageInterceptor()
{
DynamicDataSourceInterceptor interceptor = new DynamicDataSourceInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactories)
{
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
并且在classpath:META-INF/spring.factories文件中配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.realjt.smart.boot.datasource.DynamicDataSourceAutoConfiguration
至此,可以在Mapper类上使用@DataSourceSelector注解来标识访问哪个数据库。