Spring + Mybatis项目实现数据库读写分离
主要思路:通过实现AbstractRoutingDataSource类来动态管理数据源,利用面向切面思维,每一次进入service方法前,选择数据源。
1、首先pom.xml中添加aspect依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.9</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version> </dependency> <dependency>
2、实现AbstractRoutingDataSource类 作为数据源
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 实现AbstractRoutingDataSource类 作为数据源 * @author 木瓜牛奶泡咖啡 * */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { System.out.println("DynamicDataSourceHolder.getDataSouce()====="+DynamicDataSourceHolder.getDataSouce()); return DynamicDataSourceHolder.getDataSouce(); } }
3、用ThreadLcoal管理当前数据源
/** * 用ThreadLcoal管理当前数据源 * @author 木瓜牛奶泡咖啡 * */ public class DynamicDataSourceHolder { public static final ThreadLocal<String> holder = new ThreadLocal<String>(); public static void putDataSource(String name) { holder.set(name); } public static String getDataSouce() { return holder.get(); } public static void clearDataSource() { holder.remove(); } }
4、用注解的形式实现AOP管理数据源
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 用注解的形式实现AOP管理数据源 * @author 木瓜牛奶泡咖啡 * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataSource { String value(); }
5、创建切面类,将注解放在service实现类的方法前,自动设置当前数据源为注解中数据源。
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.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * 切面类 * 将注解放在service实现类的方法前,自动设置当前数据源为注解中数据源。 * @author 木瓜牛奶泡咖啡 * */ /** * 切换数据源(不同方法调用不同数据源) */ @Aspect @Order(1) @Component @EnableAspectJAutoProxy(proxyTargetClass = true) public class DataSourceAspect { @Pointcut("execution(* com.navi.shell.shop.service.*.*(..))") public void aspect() { } /** * 配置前置处理,使用在方法aspect()上注册的切入点,绑定数据源信息 */ @Before("aspect()") public void before(JoinPoint point) { Object target = point.getTarget(); String method = point.getSignature().getName(); System.out.println("method============" +method); Class<?> classz = target.getClass(); Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()) .getMethod().getParameterTypes(); try { Method m = classz.getMethod(method, parameterTypes); System.out.println(m.getName()); if (m != null && m.isAnnotationPresent(DataSource.class)) { DataSource data = m.getAnnotation(DataSource.class); System.out.println("value==========="+data.value()); DynamicDataSourceHolder.putDataSource(data.value()); } } catch (Exception e) { e.printStackTrace(); } } /** * 配置后置处理,清空数据源信息 * @param point */ @After("aspect()") public void after(JoinPoint point) { DynamicDataSourceHolder.clearDataSource(); } }
6、配置数据源applicationContext-project.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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 引入配置文件 --> <context:property-placeholder location="classpath:db.properties" ignore-unresolvable="true"/> <!-- 数据源配置 --> <bean id="dataSource_wr" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${db.url}"/> <property name="username" value="${db.username}"/> <property name="password" value="${db.password}"/> <property name="connectionProperties" value="${db.driver}"></property> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="1"/> <property name="minIdle" value="1"/> <property name="maxActive" value="20"/> <!-- 配置获取连接等待超时的时间 --> <property name="maxWait" value="60000"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="300000"/> <property name="validationQuery" value="SELECT 'x'"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="true"/> <property name="testOnReturn" value="false"/> </bean> <bean id="dataSource_r" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${db1.url}"/> <property name="username" value="${db1.username}"/> <property name="password" value="${db1.password}"/> <property name="connectionProperties" value="${db.driver}"></property> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="1"/> <property name="minIdle" value="1"/> <property name="maxActive" value="20"/> <!-- 配置获取连接等待超时的时间 --> <property name="maxWait" value="60000"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="300000"/> <property name="validationQuery" value="SELECT 'x'"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="true"/> <property name="testOnReturn" value="false"/> </bean> <bean id="dataSource" class="com.ifeng.auto.we_provider.common.db.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <!-- write --> <entry key="write" value-ref="dataSource_wr"/> <!-- read --> <entry key="read" value-ref="dataSource_r"/> </map> </property> <property name="defaultTargetDataSource" ref="dataSource_wr"/> </bean> <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 自动扫描mapping.xml文件--> <!-- <property name="mapperLocations" value="classpath:com/ifeng/auto/we_provider/mapping/*.xml" /> --> <property name="mapperLocations" value="classpath:mapping/*.xml"/> </bean>
<!-- 配置SQLSession模板 -->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 设定transactionManager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 使用annotation定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
<!-- DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ifeng.auto.we_provider.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>
8、在service实现类中添加注解
@DataSource("write") public void savetag(UserTag userTag) { userTagMapper.addUserTag(userTag); } @DataSource("read") public UserTag getUserTagByUUID(String uuid) { return userTagMapper.getUserTagByUUID(uuid); }
自此数据库读写分离实现。
在配置过程中遇到的问题:
1、起初配置DataSourceAspect类的时候将其配置在applicationContext-project.xml,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/> <bean id="dataSourceAspect" class="com.ifeng.auto.we_provider.common.proxy.DataSourceAspect"/> <aop:config> <aop:aspect id="c" ref="dataSourceAspect" order="-9999"> <aop:pointcut id="tx" expression="execution(* com.ifeng.auto.we_provider.service..*.*(..))"/> <aop:before pointcut-ref="tx" method="before"/> </aop:aspect> </aop:config>
这样配置的结果导致在正常调用被注释过的service时,无法进入切面类,网上说是因为有事务的原因导致,说数据库事务的优先级总是高于AOP导致,所有我在aop配置文件中加入了“order=-9999”,这样aop的优先级总最高了吧,但是依然没有起作用,纠结了半天,通过注解的方式写在切面类中,竟然可以了,这两种方式应该是一样的,但实际却不同,不知道是什么原因。
2、之前在切面类中,少加了 public void after(JoinPoint point),导致在查询一次注解为read的service后,在去请求未被注解的service(没有注解默认走write),却走了read,原因就是少加了after的处理,加入后就好了。
3、记得要在applicationContext-project.xml中添加<context:component-scan base-package="com.navi.common.db" />
参考博客:http://blog.csdn.net/byp502955177/article/details/68927230