项目4的高级实现思路大解密。
在做项目的过程中,有时候一个数据源是不够,那么就需要配置多个数据源。本例介绍mybatis多数据源配置
前言
一般项目单数据源,使用流程如下:
单个数据源绑定给sessionFactory,再在Dao层操作,若多个数据源的话,那不是就成了下图
可见,sessionFactory都写死在了Dao层,若我再添加个数据源的话,则又得添加一个sessionFactory。所以比较好的做法应该是下图
实现原理
1、扩展Spring的AbstractRoutingDataSource抽象类(该类充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上。)
从AbstractRoutingDataSource的源码中:
1 public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean
2、我们可以看到,它继承了AbstractDataSource,而AbstractDataSource不就是javax.sql.DataSource的子类,So我们可以分析下它的getConnection方法:
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
3、 获取连接的方法中,重点是determineTargetDataSource()方法,看源码:
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
上面这段源码的重点在于determineCurrentLookupKey()方法,这是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
看完源码,应该有点启发了吧,没错!你要扩展AbstractRoutingDataSource类,并重写其中的determineCurrentLookupKey()方法,来实现数据源的切换
案例
1、搭建一个Springmvc + Spring + Mybatis maven项目,POM文件中引入AOP相关依赖,2、编辑一个扩展AbstractRoutingDataSource类,DynamicDataSource.java
package com.test.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源(依赖于spring)
* @author peter huang
* @date 2019-08-03 17:27:35
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSource();
}
}
3、 封装一个的对数据源进行操作的类,DataSourceHolder.java
package com.test.datasource;
public class DataSourceHolder {
// 线程本地环境
private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
// 设置数据源
public static void setDataSource(String customerType) {
dataSources.set(customerType);
}
// 获取数据源
public static String getDataSource() {
return (String) dataSources.get();
}
// 清除数据源
public static void clearDataSource() {
dataSources.remove();
}
}
4、当需要切换数据源的时候执行啦。手动在代码中调用写死吗?调用setDataSource方法
但是这种方法比较死板,所以我们可以应用spring aop来设置,把配置的数据源类型都设置成为注解标签,在service层中需要切换数据源的方法上,写上注解标签,调用相应方法切换数据源咯(就跟你设置事务一样)
1 @TargetDataSource(name=TargetDataSource.SLAVE)
2 publicList getEmpsFromSalve()
编辑注解标签TargetDataSource.java
package com.test.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name() default TargetDataSource.MASTER;
public static String MASTER = "dataSource1";
public static String SLAVE = "dataSource2";
}
5、编辑切面的Bean,DataSourceExchange.java
package com.test.datasource;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import com.test.annotation.TargetDataSource;
public class DataSourceExchange implements MethodBeforeAdvice, AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
DataSourceHolder.clearDataSource();
}
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
// 这里TargetDataSource是自定义的注解
if (method.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource datasource = method.getAnnotation(TargetDataSource.class);
DataSourceHolder.setDataSource(datasource.name());
} else {
if(target.getClass().isAnnotationPresent(TargetDataSource.class))
{
TargetDataSource datasource = target.getClass().getAnnotation(TargetDataSource.class);
DataSourceHolder.setDataSource(datasource.name());
}
}
}
}
6、配置文件
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
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://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 引入数据库的配置文件 -->
<context:property-placeholder location="classpath:dbconfig.properties" />
<bean id="dataSource1" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="jdbcUrl" value="${datasource1.jdbc.url}"></property>
<property name="driverClass" value="${datasource1.jdbc.driver}"></property>
<property name="user" value="${datasource1.jdbc.username}"></property>
<property name="password" value="${datasource1.jdbc.password}"></property>
</bean>
<bean id="dataSource2" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="jdbcUrl" value="${datasource2.jdbc.url}"></property>
<property name="driverClass" value="${datasource2.jdbc.driver}"></property>
<property name="user" value="${datasource2.jdbc.username}"></property>
<property name="password" value="${datasource2.jdbc.password}"></property>
</bean>
<!-- 数据源:Spring用来控制业务逻辑。数据源、事务控制、aop -->
<bean id="dataSource" class="com.test.datasource.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSource1" value-ref="dataSource1"></entry>
<entry key="dataSource2" value-ref="dataSource2"></entry>
</map>
</property>
<!-- 默认目标数据源为你主库数据源 -->
<property name="defaultTargetDataSource" ref="dataSource1"/>
</bean>
<!-- spring事务管理 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启基于注解的事务 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" order="2"/>
<!--
整合mybatis
目的:1、spring管理所有组件。mapper的实现类。
service==>Dao @Autowired:自动注入mapper;
2、spring用来管理事务,spring声明式事务
-->
<!--创建出SqlSessionFactory对象 -->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<!-- configLocation指定全局配置文件的位置 -->
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<!--mapperLocations: 指定mapper文件的位置-->
<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"></property>
</bean>
<!--配置一个可以进行批量执行的sqlSession -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactoryBean"></constructor-arg>
<constructor-arg name="executorType" value="BATCH"></constructor-arg>
</bean>
<!-- 扫描所有的mapper接口的实现,让这些mapper能够自动注入;
base-package:指定mapper接口的包名
-->
<mybatis-spring:scan base-package="com.test.dao"/>
<!-- 配置切面的Bean -->
<bean id="dataSourceExchange" class="com.test.datasource.DataSourceExchange"/>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut id="servicePointcut" expression="execution(* com.test.service.*.*(..))"/>
<!-- 关键配置,切换数据源一定要比持久层代码更先执行(事务也算持久层代码) <aop:advisor advice-ref="txAdvice" pointcut-ref="service" order="2"/> -->
<aop:advisor advice-ref="dataSourceExchange" pointcut-ref="servicePointcut" order="1"/>
</aop:config>
</beans>
注意:Spring中的事务是通过aop来实现的,当我们自己写aop拦截的时候,会遇到跟spring的事务aop执行的先后顺序问题,比如说动态切换数据源的问题,如果事务在前,数据源切换在后,会导致数据源切换失效,所以就用到了Order(排序)这个关键字
1<aop:advisor advice-ref="dataSourceExchange" pointcut-ref="servicePointcut" order="1"/>
1<!-- 开启基于注解的事务 -->2<tx:annotation-driven transaction-manager="dataSourceTransactionManager" order="2"/>
7、在service上加上注解即可使用
1@Transactional
2@TargetDataSource(name=TargetDataSource.SLAVE)
3publicint addEmployeeFromSalve(Employee employee)
{4
5return employeeMapper.insert(employee);
6}
数据流转顺序:
1.xml<aop>拦截到数据源名称
2.执行切面DataSourceExchange中的before方法,将数据源名称放入 DataSourceHolder中
3.Spring 调用determineCurrentLookupKey()方法<DynamicDataSource中重写AbstractRoutingDataSource类中的方法> ,从DataSourceHolder取出当前的数据库名称,并返回
4.AbstractRoutingDataSource类中determineTargetDataSource()方法调用determineCurrentLookupKey()匹配到指定的数据库,并建立链接,即为切换到相应的数据库;
5.在指定的数据库中执行相应的sql