天天看点

真正意义的Spring动态切换数据源

写这篇博文的目的,是因为网上虽有林林总总的各种spring动态切换数据源教程,但是都不太满足我的需求,大部分教程都能实现切换数据源,但是在我看来,都没有真正意义的实现动态切换,举个例子,许多教程使用@DataSource(value="xxx")来切换数据源,我并不太认同这种方式。

       我所理解的动态切换数据源,不仅仅是使用@DataSource(value="xxx")切换数据源,而是能根据指定的数据源实现真正意义的动态切换。

否则,单纯使用@DataSource(value="xxx")没有实际业务意义,假设如下业务场景:同一段业务代码,针对不同部门的用户,需要从不同的数据源查询出数据,如果使用@DataSource(value="xxx")的方式,则无法满足业务需求,因为@DataSource(value="xxx")每次只能指定某一个数据源,如果要切换不同的数据源,只能改代码,这明显不现实,因此,我采用了另外一种解决方案:即在方法体传入指定数据源作为参数,使用AOP和反射完成真正的动态切换数据源。

1.配置多数据源

<context:property-placeholder location="classpath:jdbc.properties" />
 
	<bean id="dataSource_main" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="driverClass" value="${jdbc.main.driver}" />
		<property name="jdbcUrl" value="${jdbc.main.url}" />
		<property name="user" value="${jdbc.main.username}" />
		<property name="password" value="${jdbc.main.password}" />
	</bean>
	<bean id="dataSource_copy" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="driverClass" value="${jdbc.copy.driver}" />
		<property name="jdbcUrl" value="${jdbc.copy.url}" />
		<property name="user" value="${jdbc.copy.username}" />
		<property name="password" value="${jdbc.copy.password}" />
	</bean>
	
	<bean id="dataSource" class="com.juncheng.template.datasource.DynamicDataSource">
        <property name="defaultTargetDataSource" ref="dataSource_main"> </property>
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry value-ref="dataSource_main" key="dataSource_main"/>
                <entry value-ref="dataSource_copy" key="dataSource_copy"/>
            </map>
        </property>
    </bean>

	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:mybatis-config.xml" />
	</bean>

	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
		<property name="basePackage" value="com.juncheng.template.**.dao" />
	</bean>

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

    <tx:annotation-driven transaction-manager="txManager"/>
           

 2.Spring对动态切换数据源的支持

  • spring支持动态切换数据源,在spring中有一个抽象类AbstractRoutingDataSource类,通过这个类可以实现动态选择数据源。
private Map<Object, Object> targetDataSources;

	private Object defaultTargetDataSource;

        protected abstract Object determineCurrentLookupKey();
           
  • AbstractRoutingDataSource中有两个最重要的参数和一个抽象方法,targetDataSources是能进行切换的所有数据源的Map对象,defaultTargetDataSource是默认的数据源对象。
  • determineCurrentLookupKey()抽象方法是决定当前使用哪个数据源,即对应的targetDataSources的key。因此,我们需要自己创建一个类,继承自AbstractRoutingDataSource,实现其determineCurrentLookupKey方法。
  • 了解了这些,我们再返回去看上面XML的配置:首先来看DynamicDataSource
    public class DynamicDataSource extends AbstractRoutingDataSource {
    	@Override
    	protected Object determineCurrentLookupKey() {
    		return DataSourceContextHolder.getDataSource();
    	}
    }
               
    该类就是上述所说继承自AbstractRoutingDataSource,实现其determineCurrentLookupKey方法的类,方法体中只需要返回需要切换的数据源的key,该key值对应于targetDataSources中的key,因此方法的实现要保证通过key能在targetDataSources找到对应的数据源。DataSourceContextHolder的代码如下:
    public class DataSourceContextHolder {
    	private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    	/**
    	 * @Description: 设置数据源类型
    	 * @param dataSourceType  数据库类型
    	 * @return void
    	 * @throws
    	 */
    	public static void setDataSource(String dataSource) {
    		contextHolder.set(dataSource);
    	}
    	
    	/**
    	 * @Description: 获取数据源类型
    	 * @param 
    	 * @return String
    	 * @throws
    	 */
    	public static String getDataSource() {
    		return contextHolder.get();
    	}
    	
    	/**
    	 * @Description: 清除数据源类型
    	 * @param 
    	 * @return void
    	 * @throws
    	 */
    	public static void clearDataSource() {
    		contextHolder.remove();
    	}
    }
               
    该类用了ThreadLocal来存储需要切换的数据源的key。至于为何用ThreadLocal,因为spring的bean基本都是单例模式,为了避免多用户同时访问的线程安全问题而采用ThreadLocal,因为ThreadLocal是线程安全的。 
  • 由于DynamicDataSource继承自AbstractRoutingDataSource,因此需要在XML中配置上述我们所说的最重要的两个参数:targetDataSources和defaultTargetDataSource
    <bean id="dataSource" class="com.juncheng.template.datasource.DynamicDataSource">
            <property name="defaultTargetDataSource" ref="dataSource_main"> </property>
            <property name="targetDataSources">
                <map key-type="java.lang.String">
                    <entry value-ref="dataSource_main" key="dataSource_main"/>
                    <entry value-ref="dataSource_copy" key="dataSource_copy"/>
                </map>
            </property>
        </bean>
               
    另外,建议targetDataSources中的map的entry对象的key和value-ref保持一致,至于原因,后面我们会谈到。

3.使用AOP实现service方法级别的动态数据源切换

  • 首先,我们需要自定义一个注解,用于在AOP中进行处理:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
  
    String value() default "";
    
}
           
  • 然后是DataSourceAspect:
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DataSourceAspect implements ApplicationContextAware {

	private static ApplicationContext appCtx;

	private static final Logger LOG = Logger.getLogger(DataSourceAspect.class);

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		appCtx = applicationContext;
	}

	@Before("execution(* com.juncheng.template..*.*ServiceImpl.*(..))")
	public void setDataSoruce(JoinPoint joinPoint) throws Throwable {
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		Object[] args = joinPoint.getArgs();
		if (method.isAnnotationPresent(DataSource.class) && BaseUtil.arrayNotNull(args) && args[0] != null) {
			String dataSourceKey = String.valueOf(args[0]);
			ComboPooledDataSource dataSource = null;
			try {
				dataSource = (ComboPooledDataSource) appCtx.getBean(dataSourceKey);
			} catch (Exception e) {
				LOG.info("数据源切换失败,数据源[" + dataSourceKey + "]未配置!, e.getCause:" + e.getCause());
				throw new Exception("数据源切换失败,数据源[" + dataSourceKey + "]未配置!");
			}
			Connection connect = null;
			try {
				connect = dataSource.getConnection();
			} catch (Exception e) {
				LOG.info("数据源切换失败,数据源[" + dataSourceKey + "]链接失败,请检查用户名密码是否正确!");
				throw new Exception("数据源切换失败,数据源[" + dataSourceKey + "]链接失败,请检查用户名密码是否正确!");
			}
			// 主动关闭数据库链接
			if (connect != null) {
				connect.close();
			}
			DataSourceContextHolder.setDataSource(dataSourceKey);
		}
	}


	@After("execution(* com.juncheng.template..*.*ServiceImpl.*(..))")
	public void removeDataSoruce(JoinPoint joinPoint) throws Throwable {
		DataSourceContextHolder.clearDataSource();
	}
}
           

整个项目,最核心的部分即是DataSourceAspect,首先确定切入点:由于是service方法级的拦截(不推荐在service方法内再次进行数据源切换,否则对数据源的切换应放到DAO层),因此切入点毋庸置疑是所有service方法:execution(* com.juncheng.template..*.*ServiceImpl.*(..)),再确定通知类型,由于对方法进行拦截,逻辑是在方法执行前进行数据源切换,方法完成后(包括异常)再切回默认数据源,因此我们选用前置通知和最终通知,这样整个切面就出来了。

再来看看在前置通知中都做了些什么:

@Before("execution(* com.juncheng.template..*.*ServiceImpl.*(..))")
	public void setDataSoruce(JoinPoint joinPoint) throws Throwable {
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		Object[] args = joinPoint.getArgs();
		if (method.isAnnotationPresent(DataSource.class) && BaseUtil.arrayNotNull(args) && args[0] != null) {
			String dataSourceKey = String.valueOf(args[0]);
			ComboPooledDataSource dataSource = null;
			try {
				dataSource = (ComboPooledDataSource) appCtx.getBean(dataSourceKey);
			} catch (Exception e) {
				LOG.info("数据源切换失败,数据源[" + dataSourceKey + "]未配置!, e.getCause:" + e.getCause());
				throw new Exception("数据源切换失败,数据源[" + dataSourceKey + "]未配置!");
			}
			Connection connect = null;
			try {
				connect = dataSource.getConnection();
			} catch (Exception e) {
				LOG.info("数据源切换失败,数据源[" + dataSourceKey + "]链接失败,请检查用户名密码是否正确!");
				throw new Exception("数据源切换失败,数据源[" + dataSourceKey + "]链接失败,请检查用户名密码是否正确!");
			}
			// 主动关闭数据库链接
			if (connect != null) {
				connect.close();
			}
			DataSourceContextHolder.setDataSource(dataSourceKey);
		}
	}
           

在前置通知中,首先我们利用反射获取到执行方法,判断方法体如果有@DataSource注解,则从方法参数中获取需要切换的数据源,即我之前所说的:从方法体传入指定数据源作为参数,使用AOP和反射完成真正的动态切换数据源。如果判断有该数据源(这里即是回应上述我所说的建议targetDataSources中的map的entry对象的key和value-ref保持一致,否则appCtx.getBean(dataSourceKey)还需要进行转换才能获取到正确的数据库连接)并且能成功连接,则调用DataSourceContextHolder.setDataSource(dataSourceKey)方法设置需要切换的数据源的key,然后spring在DynamicDataSource中获取该key,最终完成动态数据源切换。当方法执行完成后,在最终通知中,我们调用DataSourceContextHolder.clearDataSource()清除该数据源。

另外,如果采用spring声明式事务管理,@Order(Ordered.LOWEST_PRECEDENCE - 1)(另一种方式是@Order(1)以及XML中<tx:annotation-driven transaction-manager="txManager" order="2"/> 配置事务的order为2,个人觉得1和2的方式不利益阅读)是非常重要的,因为@Transactional也是基于AOP实现的,多个AOP执行存在一个顺序问题,order越小,执行优先级越高,在事务管理之前,必须要有数据源,即切换数据源要在事务管理之前执行,因此需要设置动态切换数据源的order为@Order(Ordered.LOWEST_PRECEDENCE - 1)(@Transactional的order使用默认值为最小值)确保切换数据源在事务管理之前,否则切换数据源以及事务管理会失败。

4.service层方法设计以及使用

在上面的切面中,我们看到这段代码:

if (method.isAnnotationPresent(DataSource.class) && BaseUtil.arrayNotNull(args) && args[0] != null) {
    ......
}
           

因此,我们的service层应该如下设计:

public interface IEmployeeService{

	Employee findById(Integer id);
	
	@DataSource
	Employee findById(String dataSourceKey,Integer id);
}
           

在含有@DataSource注解的方法体中,第一个参数应该是需要切换的数据源key,然后方法实现直接调用没有@DataSource注解的同名方法:

@Service
public class EmployeeServiceImpl implements IEmployeeService{

	@Autowired
	private EmployeeDao employeeDao;

	@Override
	public Employee findById(Integer id) {
		return employeeDao.findById(id);
	}

	@Override
	public Employee findById(String dataSourceKey, Integer id) {
		return this.findById(id);
	}
	
}
           

最后测试结果如下:

controller层代码:

@RequestMapping("dataSourceChangeTest")
	public ModelAndView getTest(HttpServletRequest request,String dataSourceKey){
		ModelAndView modelAndView = new ModelAndView();
		Employee employee = null;
		if(!BaseUtil.isEmpty(dataSourceKey)){
			employee = employeeService.findById(dataSourceKey,2);
		}else{
			employee = employeeService.findById(1);
		}
		modelAndView.addObject("employee", employee);
		modelAndView.setViewName("changeDataSource");
		return modelAndView;
	}
           

dataSource_main数据库对应的数据:

真正意义的Spring动态切换数据源

dataSource_copy数据库对应的数据:

真正意义的Spring动态切换数据源

访问地址不输入dataSourceKey的时候,访问到的是dataSource_main的数据:

真正意义的Spring动态切换数据源

访问地址输入dataSourceKey=dataSource_copy的时候,访问到的是dataSource_copy的数据:

真正意义的Spring动态切换数据源

5.源码下载

最后,附上源码下载地址:源码下载地址。转载请注明出处。