做一个积极的人
编码、改bug、提升自己
我有一个乐园,面向编程,春暖花开!
在做项目的时候遇到需要从两个数据源获取数据,项目使用的Spring + Mybatis环境,看到网上有一些关于多数据源的配置,自己也整理学习一下,然后自动切换实现从不同的数据源获取数据功能。
DataSourceConstants
数据源常量类 /** * 数据源名称常量类 * 对应 application.xml 中 bean multipleDataSource * @author:dufy * @version:1.0.0 * @date 2018/12/17 */ public class DataSourceConstants { /** * 数据源1,默认数据源配置 */ public static final String DATASOURCE_1 = "dataSource1"; /** * 数据源2 */ public static final String DATASOURCE_2 = "dataSource2"; } 复制代码
DataSourceType
自定义数据源注解 /** * 自定义数据源类型注解 * * @author:dufy * @version:1.0.0 * @date 2018/12/17 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSourceType { String value() default DataSourceConstants.DATASOURCE_1; } 复制代码
MultipleDataSource
多数据源配置类 MultipleDataSource
继承 AbstractRoutingDataSource
类,为什么继承这个类就可以了?请看 第五章 :实现原理。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 自定义多数据源配置类 * * @author:dufy * @version:1.0.0 * @date 2018/12/17 */ public class MultipleDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<String>(); /** * 设置数据源 * @param dataSource 数据源名称 */ public static void setDataSource(String dataSource){ dataSourceHolder.set(dataSource); } /** * 获取数据源 * @return */ public static String getDatasource() { return dataSourceHolder.get(); } /** * 清除数据源 */ public static void clearDataSource(){ dataSourceHolder.remove(); } @Override protected Object determineCurrentLookupKey() { return dataSourceHolder.get(); } } 复制代码
MultipleDataSourceAop
多数据源自动切换通知类 注意:请设置 @Order(0)。否则可能出现 数据源切换失败问题! 因为要在事务开启之前就进行判断,并进行切换数据源!
/** * 多数据源自动切换通知类<br> * <p> * 首先判断当前类是否被该DataSourceType注解进行注释,如果没有指定注解,则采用默认的数据源配置; <br> * 如果有,则读取注解中的value值,将数据源切到value指定的数据源 * * @author:dufy * @version:1.0.0 * @date 2018/12/17 */ @Aspect // for aop @Component // for auto scan @Order(0) // execute before @Transactional public class MultipleDataSourceAop { private final Logger logger = Logger.getLogger(MultipleDataSourceAop.class); /** * 拦截 com.**.servicee中所有的方法,根据配置情况进行数据源切换 * com.jiuling.tz.service * com.jiuling.web.service * @param joinPoint * @throws Throwable */ @Before("execution(* com.dufy.*.service.*.*(..))") public void changeDataSource(JoinPoint joinPoint) throws Throwable { try { // 拦截的实体类,就是当前正在执行的service Class<?> clazz = joinPoint.getTarget().getClass(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 提取目标对象方法注解和类型注解中的数据源标识 Class<?>[] types = method.getParameterTypes(); if (clazz.isAnnotationPresent(DataSourceType.class)) { DataSourceType source = clazz.getAnnotation(DataSourceType.class); MultipleDataSource.setDataSource(source.value()); logger.info("Service Class 数据源切换至--->" + source.value()); } Method m = clazz.getMethod(method.getName(), types); if (m != null && m.isAnnotationPresent(DataSourceType.class)) { DataSourceType source = m.getAnnotation(DataSourceType.class); MultipleDataSource.setDataSource(source.value()); logger.info("Service Method 数据源切换至--->" + source.value()); } } catch (Exception e) { e.printStackTrace(); } } /** * 方法结束后 */ @After("execution(* com.dufy.*.service.*.*(..))") public void afterReturning() throws Throwable { try { MultipleDataSource.clearDataSource(); logger.debug("数据源已移除!"); } catch (Exception e) { e.printStackTrace(); logger.debug("数据源移除报错!"); } } } 复制代码
在 applicationContext.xml
中配置详情
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p" xmlns:util="http://www.springframework.org/schema/util" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <!-- 自动扫描包 ,将带有注解的类 纳入spring容器管理 --> <context:component-scan base-package="com.dufy"></context:component-scan> <!-- 引入配置文件 --> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath*:jdbc.properties</value> </list> </property> </bean> <!-- dataSource1 配置 --> <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 基本属性 url、user、password --> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="${ds.initialSize}"/> <property name="minIdle" value="${ds.minIdle}"/> <property name="maxActive" value="${ds.maxActive}"/> <!-- 配置获取连接等待超时的时间 --> <property name="maxWait" value="${ds.maxWait}"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${ds.timeBetweenEvictionRunsMillis}"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${ds.minEvictableIdleTimeMillis}"/> <property name="validationQuery" value="SELECT 'x'"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <!-- 打开PSCache,并且指定每个连接上PSCache的大小 --> <property name="poolPreparedStatements" value="false"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/> <!-- 配置监控统计拦截的filters --> <property name="filters" value="stat"/> </bean> <!-- dataSource2 配置--> <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 基本属性 url、user、password --> <property name="url" value="${jd.jdbc.url}"/> <property name="username" value="${jd.jdbc.username}"/> <property name="password" value="${jd.jdbc.password}"/> <!-- 其他配置省略 --> </bean> <!--多数据源配置--> <bean id="multipleDataSource" class="com.jiuling.core.ds.MultipleDataSource"> <property name="defaultTargetDataSource" ref="dataSource1" /> <property name="targetDataSources"> <map key-type = "java.lang.String"> <entry key="dataSource1" value-ref="dataSource1"/> <entry key="dataSource2" value-ref="dataSource2"/> <!-- 这里还可以加多个dataSource --> </map> </property> </bean> <!-- mybatis文件配置,扫描所有mapper文件 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="multipleDataSource" p:configLocation="classpath:mybatis-config.xml" p:mapperLocations="classpath:com/dufy/*/dao/*.xml"/> <!-- spring与mybatis整合配置,扫描所有dao --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:basePackage="com.dufy.*.dao" p:sqlSessionFactoryBeanName="sqlSessionFactory"/> <!-- 对dataSource 数据源进行事务管理 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="multipleDataSource"/> <!-- 事务管理 通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 对insert,update,delete 开头的方法进行事务管理,只要有异常就回滚 --> <tx:method name="insert*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/> <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/> <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/> <!-- select,count开头的方法,开启只读,提高数据库访问性能 --> <tx:method name="select*" read-only="true"/> <tx:method name="count*" read-only="true"/> <!-- 对其他方法 使用默认的事务管理 --> <tx:method name="*"/> </tx:attributes> </tx:advice> <!-- 事务 aop 配置 --> <aop:config> <aop:pointcut id="serviceMethods" expression="execution(* com.dufy.*.service..*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/> </aop:config> <!-- 配置使Spring采用CGLIB代理 --> <aop:aspectj-autoproxy proxy-target-class="true"/> <!-- 启用对事务注解的支持 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans> 复制代码
jdbc.properties
配置内容
##-------------mysql数据库连接配置 ---------------------### # dataSource1 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://192.168.1.110:3306/jdsc?useUnicode=true&characterEncoding=utf-8 jdbc.username=test jdbc.password=123456 #配置初始化大小、最小、最大 ds.initialSize=1 ds.minIdle=1 ds.maxActive=20 #配置获取连接等待超时的时间 ds.maxWait=60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 ds.timeBetweenEvictionRunsMillis=60000 #配置一个连接在池中最小生存的时间,单位是毫秒 ds.minEvictableIdleTimeMillis=300000 # dataSource2 jd.jdbc.url=jdbc:mysql://192.168.1.120:3306/jdsc?useUnicode=true&characterEncoding=utf-8 jd.jdbc.username=root jd.jdbc.password=123456 复制代码
注意:测试服务类的包路径,因为只有被AOP拦截的到的指定的Service才会进行数据源的切换。
package com.dufy.web.service.impl.Data1ServiceImpl; /** * 使用 dataSourc1 ,配置dataSourc1的数据源 * @author:duf * @version:1.0.0 * @date 2018/12/17 */ @Service @DataSourceType(value = DataSourceConstants.DATASOURCE_1) public class Data1ServiceImpl implements Data1Service { @Resource private Data1Mapper data1Mapper; @Override public List<String> selectCaseByUpdateTime(String name) { List<String> data1 = data1Mapper.selectData1(name); return data1; } } package com.dufy.web.service.impl.Data2ServiceImpl; /** * 使用 dataSourc2 ,配置dataSourc2的数据源 * @author:duf * @version:1.0.0 * @date 2018/12/17 */ @Service @DataSourceType(value = DataSourceConstants.DATASOURCE_2) public class Data2ServiceImpl implements Data2Service { @Resource private Data2Mapper data2Mapper; @Override public List<String> selectCaseByUpdateTime(String name) { List<String> data2 = data2Mapper.selectData2(name); return data2; } } 复制代码
通过测试后发现,两个Service服务器分别调用自己的配置的数据源进行数据的获取!
基于 AbstractRoutingDataSource
实现 多数据源配置,通过AOP来进行数据源的灵活切换。AOP的相关原理这里不做说明,就简单说一下 AbstractRoutingDataSource
,它是如何切换数据源的!
首先我们继承 AbstractRoutingDataSource
它是一个抽象类,然后要实现它里面的一个抽象方法。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean 复制代码
实现了 InitializingBean
,InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。
在 AbstractRoutingDataSource
中 afterPropertiesSet
方法的实现。
public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } else { this.resolvedDataSources = new HashMap(this.targetDataSources.size()); Iterator var1 = this.targetDataSources.entrySet().iterator(); while(var1.hasNext()) { Entry<Object, Object> entry = (Entry)var1.next(); Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue()); this.resolvedDataSources.put(lookupKey, dataSource); } if (this.defaultTargetDataSource != null) { this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource); } } } 复制代码
afterPropertiesSet
方法将配置在 applicationContext.xml
中的 targetDataSources 解析并构造出一个HashMap。
然后在实际过程中当需要访问数据库的时候,会首先获取一个Connection,下面看一下获取 Connection 的方法。
public Connection getConnection() throws SQLException { return this.determineTargetDataSource().getConnection(); } public Connection getConnection(String username, String password) throws SQLException { return this.determineTargetDataSource().getConnection(username, password); } protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); // 这里需要注意,通过子类的 determineCurrentLookupKey 方法 获取 lookupKey Object lookupKey = this.determineCurrentLookupKey(); // 转换为对应的 DataSource 数据源 DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { // 如果符合上述条件,则获取默认的数据源,也就是在 ApplicationContext.xml 配置的默认数据源 dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } else { return dataSource; } } // 抽象方法,用于子类实现 protected abstract Object determineCurrentLookupKey(); 复制代码
知道了 AbstractRoutingDataSource
的抽象方法后,通过AOP拦截,将Service上面配置不同的数据源进行装配到当前请求的 ThreadLocal
中, 最后 在获取Connection的时候,就能通过 determineCurrentLookupKey
方法获取到 设置的数据源。
@Override protected Object determineCurrentLookupKey() { return dataSourceHolder.get(); } 复制代码
再次强调: 必须保证切换数据源的Aspect必须在@Transactional这个Aspect之前执行,使用@Order(0)来保证切换数据源先于@Transactional执行) 。
Spring, MyBatis 多数据源的配置和管理
spring配置多数据源涉及事务无法切换解决方案(绝对有效)
谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!
© 每天都在变得更好的阿飞云