写这篇博文的目的,是因为网上虽有林林总总的各种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
该类就是上述所说继承自AbstractRoutingDataSource,实现其determineCurrentLookupKey方法的类,方法体中只需要返回需要切换的数据源的key,该key值对应于targetDataSources中的key,因此方法的实现要保证通过key能在targetDataSources找到对应的数据源。DataSourceContextHolder的代码如下:public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }
该类用了ThreadLocal来存储需要切换的数据源的key。至于为何用ThreadLocal,因为spring的bean基本都是单例模式,为了避免多用户同时访问的线程安全问题而采用ThreadLocal,因为ThreadLocal是线程安全的。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(); } }
- 由于DynamicDataSource继承自AbstractRoutingDataSource,因此需要在XML中配置上述我们所说的最重要的两个参数:targetDataSources和defaultTargetDataSource
另外,建议targetDataSources中的map的entry对象的key和value-ref保持一致,至于原因,后面我们会谈到。<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>
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数据库对应的数据:
dataSource_copy数据库对应的数据:
访问地址不输入dataSourceKey的时候,访问到的是dataSource_main的数据:
访问地址输入dataSourceKey=dataSource_copy的时候,访问到的是dataSource_copy的数据:
5.源码下载
最后,附上源码下载地址:源码下载地址。转载请注明出处。