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

原创
2017/06/30 22:56
阅读数 985

简介和依赖

项目的前提是安装了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)方法参数

展开阅读全文
打赏
0
2 收藏
分享
加载中
更多评论
打赏
0 评论
2 收藏
0
分享
返回顶部
顶部