Spring项目中使用两种方法动态切换数据源,多数据源切换

2021年9月30日 54点热度 0条评论 来源: WuBinBin-Albert

鉴于很多同学反应,在看的过程中,自己搭建的demo,没有成功。
首先附上本项目github地址,可以对照着看,看一下相关的配置是否正确配置。

本文介绍两种动态切换数据库的方法。
方法一:数据源信息配置在xml中,适用于一般数据库切换。执行完某操作,切换数据库,执行另一个操作。
方法二:数据源信息配置在默认数据源中,适用于切换数据库操作同一方法,相当于批量执行方法。

两种方法核心都是AbstractRoutingDataSource,由spring提供,用来动态切换数据源。我们需要继承它,来进行操作。本博客中,隐藏了部分spring相关的配置信息。

源码分析可以看下一篇文章源码分析

文章目录

方法一:数据源信息都配置在xml中

1…继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class MultiDataSource extends AbstractRoutingDataSource{

	/* ThreadLocal,叫线程本地变量或线程本地存储。
	 * ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
	 * 这里使用它的子类InheritableThreadLocal用来保证父子线程都能拿到值。
	 */
	private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<String>();
	
	/**
	 * 设置dataSourceKey的值
	 * @param dataSource
	 */
	public static void setDataSourceKey(String dataSource) {
		dataSourceKey.set(dataSource);
	}
	/**
	 * 清除dataSourceKey的值
	 */
	public static void toDefault() {
		dataSourceKey.remove();
	}
	/**
	 * 返回当前dataSourceKey的值
	 */
	@Override
	protected Object determineCurrentLookupKey() {
		return dataSourceKey.get();
	}
}

2… 配置xml,这里将上面创建的MultiDataSource注入到spring容器中,这里主要用到AbstractRoutingDataSource的两个属性defaultTargetDataSource和targetDataSources。defaultTargetDataSource默认目标数据源,targetDataSources(map类型)存放用来切换的数据源。配置完以后,其他地方用到数据源的话,都引用multiDataSource。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">


	<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
		init-method="init" destroy-method="close">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
			<property name="url" value="第一个数据库的url" />
			<property name="username" value="账号" />
			<property name="password" value="密码" />
			<property name="validationQuery" value="select 1"></property>
			<property name="testOnBorrow" value="true"></property>
	</bean>
		<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource"
		init-method="init" destroy-method="close">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="第二个数据库的url" />
		<property name="username" value="账号" />
		<property name="password" value="密码" />
		<property name="validationQuery" value="select 1"></property>
		<property name="testOnBorrow" value="true"></property>
	</bean>
	<bean id="multiDataSource" class="com.wbb.dataSource.MultiDataSource">
		<property name="defaultTargetDataSource" ref="dataSource"></property>
		<property name="targetDataSources">
			<map>
				<entry key="dataSource2" value-ref="dataSource2"></entry>
			</map>
		</property>
	</bean>
	<!-- mybatis -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="multiDataSource"></property>
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<property name="mapperLocations" value="classpath:com/wbb/mapper/*.xml"></property>
	</bean>
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
		<property name="basePackage" value="com.wbb.mapper"></property>
	</bean>

	<!-- 添加事务管理 -->
	<tx:annotation-driven transaction-manager="transactionManager" />

	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="multiDataSource"></property>
	</bean>

</beans>

3…手动切换数据源,切换完以后,记得再切回默认数据库。

MultiDataSource.setDataSourceKey("dataSource2");//切换到dataSource2数据源
XXX在该数据源下的操作XXX
MultiDataSource.toDefault();//操作完以后,清除dataSourceKey的值,即切回默认数据源,原理后面会讲。

4… 利用aop切换数据源,这里记得开启aop,配置文件中使用<aop:aspectj-autoproxy />
4.1首先定义一个注解,来调用注解切换数据库

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;

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicRoutingDataSource {

	String value() default "dataSource";//本文默认dataSource

}

4.2 这里使用@Before和@After,在调用目标方法前,进行aop拦截,通过解析注解上的值来切换数据源。在调用方法结束后,切回默认数据源。如果目标方法无返回值,也可以使用@Around,调用ProceedingJoinPoint的proceed()方法前切换数据源,调用proceed()方法后切回默认数据源。

import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.wbb.annotation.DynamicRoutingDataSource;
import com.wbb.dataSource.MultiDataSource;

@Aspect
@Component
public class HandlerDataSourceAop {

	private static Logger logger = LoggerFactory.getLogger(HandlerDataSourceAop.class);
	/**
	 * @within匹配类上的注解
	 * @annotation匹配方法上的注解
	 */
	@Pointcut("@within(com.wbb.annotation.DynamicRoutingDataSource)||@annotation(com.wbb.annotation.DynamicRoutingDataSource)")
	public void pointcut(){}
	
	@Before(value = "pointcut()")
	public void beforeOpt(JoinPoint joinPoint) {
	/** 先查找方法上的注解,没有的话再去查找类上的注解
	*-----------------------------------------------------------------------
 	* 这里使用的是接口的模式,注解在实现类上,所以不能使用如下方式获取目标方法的对象,
	* 因为该方式获取的是该类的接口或者顶级父类的方法的对象.
	* MethodSignature methodSignature = (MethodSignature)point.getSignature();
     * Method method = methodSignature.getMethod();
     * DynamicRoutingDataSource annotation = method.getAnnotation(DynamicRoutingDataSource.class);
     * 通过上面代码是获取不到方法上的注解的,如果真要用上面代码来获取,可以修改aop代理模式,修改为cglib代理
     * 在xml配置文件修改为<aop:aspectj-autoproxy proxy-target-class="true" /> ,
     * proxy-target-class属性true为cglib代理,默认false为jdk动态代理 。
     * ---------------------------------------------------------
     * 本文使用是jdk动态代理, 这里使用反射的方式获取方法
	*/
	//反射获取Method 方法一
	Object target = joinPoint.getTarget();
	Class<?> clazz = target.getClass();
	Method[] methods = clazz.getMethods();
	DynamicRoutingDataSource annotation = null;
	for (Method method : methods) {
		if (joinPoint.getSignature().getName().equals(method.getName())) {
			annotation = method.getAnnotation(DynamicRoutingDataSource.class);
			if (annotation == null) {
				annotation = joinPoint.getTarget().getClass().getAnnotation(DynamicRoutingDataSource.class);
				if (annotation == null) {
					return;
				}
			}
		}
	}
	// 	反射获取Method 方法二
	//		Object[] args = joinPoint.getArgs();
	//		Class<?>[] argTypes = new Class[joinPoint.getArgs().length];
	//		for (int i = 0; i < args.length; i++) {
	//			argTypes[i] = args[i].getClass();
	//		}
	//		Method method = joinPoint.getTarget().getClass().getMethod(joinPoint.getSignature().getName(), argTypes);
	//		DynamicRoutingDataSource annotation = method.getAnnotation(DynamicRoutingDataSource.class);
	//		if (annotation == null) {
	//			annotation = joinPoint.getTarget().getClass().getAnnotation(DynamicRoutingDataSource.class);
	//			if (annotation == null) {
	//				return;
	//			}
	//		}
		
		String dataSourceName = annotation.value();
		MultiDataSource.setDataSourceKey(dataSourceName);
		logger.info("切到" + dataSourceName + "数据库");
	}
	@After(value="pointcut()")
	public void afterOpt(){
		MultiDataSource.toDefault();
		logger.info("切回默认数据库");
	}
}

4.3 使用:只需要把@DynamicRoutingDataSource注解加到方法或者类上即可

@DynamicRoutingDataSource("dataSource2")

方法一到此就结束了

方法二:数据源信息配置在数据库中

1.xml的配置,这里只需要配置一个默认的数据源就行了,因为其他的数据源都是从该数据源的数据源表中读取。

	<?xml version="1.0" encoding="UTF-8"?>
	<beans xmlns="http://www.springframework.org/schema/beans"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
		xmlns:tx="http://www.springframework.org/schema/tx"
		xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
			http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">
	
		<bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"
			init-method="init" destroy-method="close">
			<property name="driverClassName" value="com.mysql.jdbc.Driver" />
			<property name="url" value="第一个数据库的url" />
			<property name="username" value="账号" />
			<property name="password" value="密码" />
			<property name="validationQuery" value="select 1"></property>
			<property name="testOnBorrow" value="true"></property>
		</bean>
	
		<!--动态数据源相关-->
	    <bean id="dynamicDataSource" class="com.wbb.dataSource.dynamic.DynamicDataSource">
	        <property name="targetDataSources">
	            <map key-type="java.lang.String">
	                <entry key="defaultDataSource" value-ref="defaultDataSource"/>
	            </map>
	        </property>
	        <property name="defaultTargetDataSource" ref="defaultDataSource"/>
	    </bean>
		<!-- mybatis -->
		<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
			<property name="dataSource" ref="dynamicDataSource"></property>
			<property name="configLocation" value="classpath:mybatis-config.xml"></property>
			<property name="mapperLocations" value="classpath:com/wbb/mapper/*.xml"></property>
		</bean>
		<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
			<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
			<property name="basePackage" value="com.wbb.mapper"></property>
		</bean>
	
		<!-- 添加事务管理 -->
		<tx:annotation-driven transaction-manager="transactionManager" />
	
		<bean id="transactionManager"
			class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
			<property name="dataSource" ref="dynamicDataSource"></property>
		</bean>
	
	</beans>

2.数据源表的设计

DROP TABLE IF EXISTS `other_datasource`;
CREATE TABLE `other_datasource` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `datasource_name` varchar(64) DEFAULT NULL,
  `database_ip` varchar(64) DEFAULT NULL,
  `database_port` varchar(64) DEFAULT NULL,
  `database_name` varchar(64) DEFAULT NULL,
  `database_username` varchar(64) DEFAULT NULL,
  `database_password` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;


3.数据源表对应的bean

/**
 * 数据源bean构建器
 */
public class DataSourceBean {

	private final String beanName;		//注册在spring中bean名字
	private final String driverClassName; 
	private final String url;
	private final String username;
	private final String password;
	private final String validationQuery;
	private final Boolean testOnBorrow;

	public String getBeanName() {
		return beanName;
	}

	public String getDriverClassName() {
		return driverClassName;
	}

	public String getUrl() {
		return url;
	}

	public String getUsername() {
		return username;
	}

	public String getPassword() {
		return password;
	}

	public String getValidationQuery() {
		return validationQuery;
	}

	public Boolean getTestOnBorrow() {
		return testOnBorrow;
	}

	public DataSourceBean(DataSourceBeanBuilder builder) {
		this.beanName = builder.getBeanName();
		this.driverClassName = builder.getDriverClassName();
		this.url = builder.getUrl();
		this.username = builder.getUsername();
		this.password = builder.getPassword();
		this.validationQuery = builder.getValidationQuery();
		this.testOnBorrow = builder.getTestOnBorrow();
	}

	public static class DataSourceBeanBuilder {
		private String beanName;
		private String driverClassName = "com.mysql.jdbc.Driver";
		private String url = "jdbc:mysql://%s:%s/%s";
	    private String databaseIP;
	    private String databasePort;
	    private String databaseName;
		private String username;
		private String password;
		private String validationQuery = "select 1";
		private Boolean testOnBorrow = true;
		public DataSourceBeanBuilder(String beanName, String databaseIP, String databasePort, String databaseName,
				String username, String password) {
			super();
			this.beanName = beanName;
			this.databaseIP = databaseIP;
			this.databasePort = databasePort;
			this.databaseName = databaseName;
			this.username = username;
			this.password = password;
		}
		public DataSourceBeanBuilder() {
			super();
		}
		public DataSourceBeanBuilder driverClassName(String driverClassName) {
			this.driverClassName = driverClassName;
			return this;
		}
		public DataSourceBeanBuilder validationQuery(String validationQuery) {
			this.validationQuery = validationQuery;
			return this;
		}
		public DataSourceBeanBuilder testOnBorrow(Boolean testOnBorrow) {
			this.testOnBorrow = testOnBorrow;
			return this;
		}
		public String getUrl() {
			return String.format(url,this.databaseIP,this.databasePort,this.databaseName);
		}
		public String getBeanName() {
			return beanName;
		}
		public String getDriverClassName() {
			return driverClassName;
		}
		public String getDatabaseIP() {
			return databaseIP;
		}
		public String getDatabasePort() {
			return databasePort;
		}
		public String getDatabaseName() {
			return databaseName;
		}
		public String getUsername() {
			return username;
		}
		public String getPassword() {
			return password;
		}
		public String getValidationQuery() {
			return validationQuery;
		}
		public Boolean getTestOnBorrow() {
			return testOnBorrow;
		}
	}

4.这里将操作抽离出,建立数据源操作类,方便操作

/**
 * 数据源操作类
 */
public class DataSourceContext {

	private static ThreadLocal<DataSourceBean> threadLocal = new InheritableThreadLocal<DataSourceBean>();

	/**
	 * 获取数据源
	 */
	public static DataSourceBean getDataSource() {
		return threadLocal.get();
	}
	/**
	 * 设置数据源
	 */
	public static void setDataSource(DataSourceBean dataSourceBean) {
		threadLocal.set(dataSourceBean);
	}
	/**
	 * 清除数据源
	 * 清除后,数据源为默认时间
	 */
	public static void toDefault() {
		threadLocal.remove();
	}
}

5.定义AbstractRoutingDataSource的继承类DynamicDataSource,同时实现ApplicationContextAware接口,因为该方法切换数据源,需要到spring上下文中生成和获取数据源bean。

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import com.alibaba.druid.pool.DruidDataSource;


public class  DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware{

	private ApplicationContext applicationContext ;

	/**
	 * 连接数据源前,调用该方法
	 */
	@Override
	protected Object determineCurrentLookupKey() {
		//1.获取手动设置的数据源参数DataSourceBean
		DataSourceBean dataSourceBean = DataSourceContext.getDataSource();
		if(dataSourceBean == null) {
			return null;
		}
		try {
			//2.获取AbstractRoutingDataSource的targetDataSources属性,该属性存放数据源属性
			Map<Object, Object> targetSourceMap = getTargetSource();
			synchronized(this) {
				/*
				 * 3.判断targetDataSources中是否已经存在要设置的数据源bean
				 * 存在的话,则直接返回beanName
				 * 
				 */
				if(!targetSourceMap.keySet().contains(dataSourceBean.getBeanName())) {
					/*不存在,则进行以下几步
					3.1 先在spring容器中创建该数据源bean
					*/
					Object dataSource = createDataSource(dataSourceBean);
					//3.2 在创建后的bean,放入到targetDataSources Map中
					targetSourceMap.put(dataSourceBean.getBeanName(), dataSource);
					/*
					 * 3.3 通知spring有bean更新
					 * 主要更新AbstractRoutingDataSource的resolvedDefaultDataSource(Map)属性,
					 * 更新完以后,AbstractRoutingDataSource的determineTargetDataSource()中,才能找到数据源
					 * 代码如下:
					 * Object lookupKey = determineCurrentLookupKey();
					   DataSource dataSource = this.resolvedDataSources.get(lookupKey);
					 */
					super.afterPropertiesSet();
				}
			}
			for(Map.Entry<Object, Object> entry : targetSourceMap.entrySet()) {
				System.out.println(entry.getKey()+"-"+entry.getValue());
			}
			return dataSourceBean.getBeanName();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	/**
	 * 根据数据源信息在spring中创建bean,并返回
	 * @param dataSourceBean 数据源信息
	 * @return
	 * @throws IllegalAccessException
	 */
	public Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {
		//1.将applicationContext转化为ConfigurableApplicationContext
		ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
		//2.获取bean工厂并转换为DefaultListableBeanFactory
		DefaultListableBeanFactory beanFactory =  (DefaultListableBeanFactory) context.getBeanFactory();
		/*
		 * 3.本文用的是DruidDataSource,所有在这里我们获取的是该bean的BeanDefinitionBuilder,
		 * 通过BeanDefinitionBuilder来创建bean定义
		 */
		BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
		/**
		 * 4.获取DataSourceBean里的属性和对应值,并将其交给BeanDefinitionBuilder创建bean的定义
		 */
		Map<String, Object> propertyKeyValues = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);
		for(Map.Entry<String,Object> entry : propertyKeyValues.entrySet()) {
			beanDefinitionBuilder.addPropertyValue(entry.getKey(), entry.getValue());
		}
		//5.bean定义创建好以后,将其交给beanFactory注册成bean对象,由spring容器管理
		beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());
		//6.最后获取步骤5生成的bean,并将其返回
		return context.getBean(dataSourceBean.getBeanName());
	}
	//获取类属性和对应的值,放入Map中
    @SuppressWarnings("unused")
	private <T> Map<String, Object> getPropertyKeyValues(Class<T> clazz, Object object) throws IllegalAccessException {
       Field[] fields = clazz.getDeclaredFields();
       Map<String,Object> map = new HashMap<>();
       for (Field field : fields) {
    	   field.setAccessible(true);
    	   map.put(field.getName(), field.get(object));
       }
       map.remove("beanName");
       return map;
    }
    //通过反射获取AbstractRoutingDataSource的targetDataSources属性
	@SuppressWarnings("unchecked")
	public Map<Object, Object> getTargetSource() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
		Field field = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");
		field.setAccessible(true);
		return (Map<Object, Object>) field.get(this);
	}
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
			this.applicationContext = applicationContext;
	}
 
}

6.测试用例测试一下

	@Test
	public void testDynamicDataSource() {
		1.从表中获取需要切换的数据源信息
		List<DataSourceDO> dataSourceDOList = dataSourceMapper.getAllDataSources();
		2.循环遍历,切换数据源,执行操作,切完就得切回默认数据源
		for (DataSourceDO dataSourceDO : dataSourceDOList) {
			DataSourceBean dataSourceBean = new DataSourceBean(new DataSourceBeanBuilder(dataSourceDO.getDatasourceName(),
					dataSourceDO.getDatabaseIp(), dataSourceDO.getDatabasePort(), dataSourceDO.getDatabaseName(),
					dataSourceDO.getUsername(), dataSourceDO.getPassword()));
			DataSourceContext.setDataSource(dataSourceBean);
			XXX你的操作XXX
			DataSourceContext.toDefault();
		}
	}

方法二部分参考https://blog.csdn.net/yizhenn/article/details/53965552该博客的思想。

源码分析可以看下一篇文章https://blog.csdn.net/u013034378/article/details/81661706

讲到这里,本篇文章到此也就结束了,如果文章中有问题,或者有一些不够严谨完善的地方,希望大家体谅体谅。欢迎大家留言,交流交流。
最后附上本项目github的地址

    原文作者:WuBinBin-Albert
    原文地址: https://blog.csdn.net/u013034378/article/details/81455513
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。