文档章节

Spring AOP之四:利用AOP实现动态数据源切换

trayvon
 trayvon
发布于 2017/06/30 22:56
字数 2369
阅读 318
收藏 2

简介和依赖

项目的前提是安装了MySQL数据库,并且建立了2个数据库一个是master,一个是slave,并且这2个数据库都有一个user表,表导出语句如下:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL COMMENT '用户名,字母数字中文',
  `password` char(64) NOT NULL COMMENT '密码,sha256加密',
  `nick_name` varchar(20) DEFAULT '' COMMENT '昵称',
  `portrait` varchar(30) DEFAULT '' COMMENT '头像,使用相对路径',
  `status` enum('valid','invalid') DEFAULT 'valid' COMMENT 'valid有效,invalid无效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';

数据库中有一个密码为123456的tim用户对这2个库有读写权限。你可以在配置文件中修改用户名和密码,也可以执行下面的语句授权给tim用户:

grant select,insert,update,delete on slave.* to tim identified by '123456'
grant select,insert,update,delete on master.* to tim identified by '123456'

或者给tim全部权限(除了grant):

grant all privileges on slave.* to tim identified by '123456'
grant all privileges on master.* to tim identified by '123456'

还是来一张工程目录的图片吧:

目录

Spring AbstractRoutingDataSource分析

我们使用org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource做动态数据源。

既然是数据源肯定直接或者间接的实现了javax.sql.DataSource接口,所以直接找getConnection方法就可以了。

我们可以看到AbstractRoutingDataSource#getConnection方法:

@Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

没有什么说的,直接看AbstractRoutingDataSource#determineTargetDataSource:

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是一个抽象方法:

protected abstract Object determineCurrentLookupKey();

resolvedDataSources是一个HashMap<String,Object> resolvedDefaultDataSource是一个DataSource

所以整个的逻辑就非常清楚了: AbstractRoutingDataSource是一个抽象类,我们只需要继承它就可以了,然后提供一个包含多个数据源的HashMap,还可以提供一个默认的数据源resolvedDefaultDataSource,然后实现determineCurrentLookupKey返回一个String类型的key,通过这个key来找到一个对应的数据源。如果没有找到就使用默认的数据源。

接下来我们就来通过继承AbstractRoutingDataSource来实现一个动态数据源。

AbstractRoutingDataSource实现

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceKeyThreadHolder.getDataSourceKey();
    }
}
import org.springframework.util.Assert;

public class DataSourceKeyThreadHolder {
    // 同一个线程持有相同的key
    private static final ThreadLocal<String> dataSourcesKeyHolder = new ThreadLocal<String>();

    public static void setDataSourceKey(String customerType) {
        Assert.notNull(customerType, "DataSourceKey cannot be null");
        dataSourcesKeyHolder.set(customerType);
    }

    public static String getDataSourceKey() {
        return dataSourcesKeyHolder.get();
    }

    public static void clearDataSourceKey() {
        dataSourcesKeyHolder.remove();
    }
}

其实完全没有必要拆分为2个类,虽然这样可以让DynamicDataSource的逻辑清晰一些,但是对于整个的来说并不一定是更加清晰的。

使用ThreadLocal让每一个线程持有一个key也只是一种手段,也可以通过其他的方式实现。

下面我们来看一下数据源的配置文件,让配置文件和DynamicDataSource对应起来:

<?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:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
    
    <bean id="design" abstract="true" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${common.driver}" />
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="10" />
        <property name="minIdle" value="10" />
        <property name="maxActive" value="60" />
        <!-- 从池中取连接的最大等待时间,单位ms -->
        <property name="maxWait" value="3000" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 测试语句 -->
        <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" />
        <!-- 打开removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分钟 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="true" />
    </bean>

    <bean id="master" parent="design">
        <property name="username" value="${master.username}" />
        <property name="password" value="${master.password}" />
        <property name="url" value="${master.url}"/>
    </bean>

    <bean id="slave" parent="design">
        <property name="username" value="${slave.username}" />
        <property name="password" value="${slave.password}" />
        <property name="url" value="${slave.url}"/>
    </bean>

    <!-- 动态数据源 -->
    <bean id="dynamicDataSource" class="cn.freemethod.datasource.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="master" value-ref="master"/>
                <entry key="slave" value-ref="slave"/>
            </map>
        </property>
        <!-- 默认数据源 -->
        <property name="defaultTargetDataSource" ref="master"/>
    </bean>
    
    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dynamicDataSource" />
        <property name="configLocation" value="classpath:design-config.xml"/>
        <property name="mapperLocations" value="classpath:mapper/design/*.xml"/>
    </bean>

    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" >
        <constructor-arg index="0" ref="sqlSessionFactory" />
    </bean>

    <!--  配置mapper的映射扫描器 根据包中定义的接口自动生成dao的实现类-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.freemethod.dao.mapper"/>
    </bean>

</beans>

我们先看一下id为design这个bean,注意这个bean配置了abstract="true",表明这个bean是不会实例化的是用来被其他bean继承的。这个bean使用的是com.alibaba.druid.pool.DruidDataSource。

接下来配置了2个数据源一个master一个slave,这2个bean继承了design。

重点看id为dynamicDataSource的bean,这个就是我们继承了AbstractRoutingDataSource的类,看到在属性为targetDataSources的Map中注入了2个数据源master和slave,key也是master和slave,默认的数据源defaultTargetDataSource配置的是master。

所以在我们的DynamicDataSource的determineCurrentLookupKey中如果返回的是master就是使用的是master数据源,如果返回的是slave使用的就是slave数据源。

上面说到的都是和动态数据源有关的没有使用到AOP啊,下面我们就介绍一下把AOP应用上。

利用AOP切换数据源

既然是利用AOP,那当然得有一个切面了,我们就先来看一下切面的代码吧。

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.springframework.stereotype.Component;


@Component
@Aspect
public class DataSourceAspect {
    

    @Pointcut("@annotation(cn.freemethod.datasource.DataSourceKey)")
//    @Pointcut("this(cn.freemethod.service.UserService)")
    public void dataSourceKey() {}

    @Before("dataSourceKey() && @annotation(dataSourceKey)")
    public void doBefore(JoinPoint point,DataSourceKey dataSourceKey) {
//        MethodSignature signature = (MethodSignature) point.getSignature();
//        Method method = signature.getMethod();
//        DataSourceKey datasource = method.getAnnotation(DataSourceKey.class);
        if (dataSourceKey != null) {
            String sourceKey = DataSourceKey.master;
            if (dataSourceKey.value().equals(DataSourceKey.master)) {
                sourceKey = DataSourceKey.master;
            } else if (dataSourceKey.value().equals(DataSourceKey.slave)) {
                sourceKey = DataSourceKey.slave;
            }
            DataSourceKeyThreadHolder.setDataSourceKey(sourceKey);
        }
    }

    @After("dataSourceKey()")
    public void doAfter(JoinPoint point) {
        DataSourceKeyThreadHolder.clearDataSourceKey();
    }
}

首先我们要明确连接点,就是要在什么地方进行切换数据源操作,这里很明确了我们要在有DataSourceKey注解的方法上进行切换数据源。

根据我们前面学习的Pointcut表达式,我们很容易的就能写出下面的表达式:

 @Pointcut("@annotation(cn.freemethod.datasource.DataSourceKey)")

通知逻辑也很简单,我们只需要把线程的上下文的key设置为方法注解上获取的数据源的key就可以了。方法执行之后再设置为之前的数据源。这样在方法执行的过程中如果使用的数据源获取到的就是方法注解上的配置对应的数据源。

看一下下面的实例怎样使用吧:

@DataSourceKey("master")
    @Override
    public int saveUserMaster(UserBean user) {
        return userBeanMapper.insertSelective(user);
    }

测试代码:

import javax.annotation.Resource;

import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import cn.freemethod.config.AspectConfig;
import cn.freemethod.dao.bean.design.UserBean;
import cn.freemethod.service.UserService;
import cn.freemethod.util.DataGenerateUtil;
//@ContextConfiguration(locations = {"classpath:spring-base.xml"})
@ContextConfiguration(classes={AspectConfig.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class UserServiceImplTest {
    
    @Resource
    UserService userServiceImpl;

    @Test
    public void testSaveUserMaster() {
        UserBean userBean = getUser();
        int actual = userServiceImpl.saveUserMaster(userBean);
        Assert.assertEquals(1, actual);
    }
    
    @Test
    public void testSaveUserSlave() {
        UserBean userBean = getUser();
        int actual = userServiceImpl.saveUserSlave(userBean);
        Assert.assertEquals(1, actual);
    }
    
    @Test
    public void testGetUser(){
        UserBean user = userServiceImpl.getUser(2);
        System.out.println(user);
    }
    
    private UserBean getUser(){
        UserBean userBean = new UserBean();
        userBean.setName(DataGenerateUtil.getAlphabet(3));
        userBean.setPassword(DigestUtils.sha256Hex(DataGenerateUtil.getAlnum(6)));
        return userBean;
    }

}

完整的代码请下载参考中的完整工程代码链接,这里之所以把测试类贴出来是因为有一个非常纠结的问题要讲,你应该也搜不到相关的资料。所以如果感兴趣的话最好把源码下载下来,然后对比着测试一下。

细心的同学可能已经注意到了切面类DataSourceAspect中注释的代码:

//        MethodSignature signature = (MethodSignature) point.getSignature();
//        Method method = signature.getMethod();
//        DataSourceKey datasource = method.getAnnotation(DataSourceKey.class);

这里有2个矛盾的地方,一个是Spring中注入的类型只能是接口类型的如测试中的:

@Resource
UserService userServiceImpl;

如果替换为:

@Resource
UserServiceImpl userServiceImpl;

就会注入失败。

但是在Spring AOP(使用AspectJ)中通过下面的代码:

 MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();

获取的不是实际委派的方法,也就是说UserService注入的实际的类型是UserServiceImpl,但是通过UserService调用方法上面得到的签名Method的是UserService的方法签名。这个有什么影响呢?在上面的例子中最直接的影响就是userServiceImpl中的方法上的注解公共上面的方法获取不到。

最开始我也是弄的我一愣一愣的,弄了很久,最后还是通过Advice 参数把注解注入到通知当中的。如下:

 @Before("dataSourceKey() && @annotation(dataSourceKey)")

关于Advice的Parameter可以参考后面的Spring AOP 之三:通知(Advice)方法参数这篇文章。

参考

项目码云链接

完整工程代码

Spring AOP 之一:基本概念与流程

Spring AOP 之二:Pointcut注解表达式

Spring AOP 之三:通知(Advice)方法参数

© 著作权归作者所有

trayvon
粉丝 16
博文 140
码字总数 212221
作品 1
程序员
私信 提问
Spring中事务与aop的先后顺序问题

Spring中的事务是通过aop来实现的,当我们自己写aop拦截的时候,会遇到跟spring的事务aop执行的先后顺序问题,比如说动态切换数据源的问题,如果事务在前,数据源切换在后,会导致数据源切换...

翊骷
2014/08/19
13.3K
8
JAVA中使用代码创建多数据源,并实现动态切换(一)

2017-06-06 11:31:57补充:近日,在本文的基础之上,扩展了下,使用atomikos来管理事务,保证多数据源操作时,事务一致性。(https://my.oschina.net/simpleton/blog/916108) 另外,感谢朋友...

十月阳光
2017/03/27
6K
0
Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源 方法

一、开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能。所以在出来数据库方言的时候基本上没有什...

ibm_hoojo
2013/10/12
0
0
使用spring动态切换数据源

原理:主要是调用目标方法时,注入不同的数据源,从而实现切换,即利用aop,而aop的实现是用代理实现的 1,给工程添加一个获取数据源的路由,并给它两个不同的数据源 2,ThreadLocalRounting...

暗中观察
01/23
24
0
使用aop动态切换数据源失败

@南湖船老大 你好,想跟你请教个问题:使用aop切换数据源,事务本事也有一个aop所以我自定义的时候实现了Spring的Ordered这个接口 并且把getOrder方法返回1 事务本身的aop的order我设置为2按...

MLGKO
2015/11/04
208
0

没有更多内容

加载失败,请刷新页面

加载更多

Google Guava 笔记

一、引言 Guava 是 google 几个java核心类库的集合,包括集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common ...

SuShine
28分钟前
7
0
SpringBoot中使用@Value为静态变量赋值并测试是否成功

今天想像普通变量一样如下采用写法取配置的,但取到的是个null。。。 @Value("${test.appKey}")private static String appKey; 才发现不能通过这种方式取配置来给static变量赋值 在网上搜索...

SilentSong
28分钟前
5
0
ECMAScript语句之with 语句

ECMAScript with 语句,用于设置代码在特定对象中的作用域(with运行缓慢,设置了属性值时更加缓慢,最好避免使用with语句) 一、with 语句用于字符串(配合toUpperCase()方法) var a = "C...

专注的阿熊
29分钟前
4
0
Apache Flink 进阶(一):Runtime 核心机制剖析

1. 综述 本文主要介绍 Flink Runtime 的作业执行的核心机制。首先介绍 Flink Runtime 的整体架构以及 Job 的基本执行流程,然后介绍在这个过程,Flink 是怎么进行资源管理、作业调度以及错误...

大涛学长
36分钟前
4
0
7. 整数反转

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。 示例 1: 输入: 123 输出: 321 示例 2: 输入: -123 输出: -321 示例 3: 输入: 120 输出: 21 注意: 假设我们的环境只能...

苏坡吴
37分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部