天天看点

Spring Boot 多数据源动态切换Spring Boot 多数据源动态切换

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注解来标识访问哪个数据库。