Spring AOP IV: Dynamic Data Source Switching Using AOP

Keywords: Spring xml Mybatis Junit

Brief Introduction and Dependence

The premise of the project is to install MySQL database, and establish two databases, one is master and the other is slave, and both databases have a user table. The table export statement is as follows:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL COMMENT 'User name,Alphanumeric Chinese',
  `password` char(64) NOT NULL COMMENT 'Password,sha256 encryption',
  `nick_name` varchar(20) DEFAULT '' COMMENT 'Nickname?',
  `portrait` varchar(30) DEFAULT '' COMMENT 'Head portrait,Using relative paths',
  `status` enum('valid','invalid') DEFAULT 'valid' COMMENT 'valid effective,invalid invalid',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='User table';

There is a tim user with password 123456 in the database who has read and write access to these two libraries. You can modify the username and password in the configuration file, or you can execute the following statement to authorize tim users:

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

Or give tim full permission (except grant):

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

Let's have a picture of the project catalogue.

Spring AbstractRouting Data Source Analysis

We use org. spring framework. jdbc. datasource. lookup. AbstractRouting DataSource as a dynamic data source.

Since the data source must directly or indirectly implement the javax.sql.DataSource interface, you can find the getConnection method directly.

We can see the AbstractRoutingDataSource#getConnection method:

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

There's nothing to say. Look directly at AbstractRouting Data Source # determineTarget Data Source:

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 is an abstract method:

protected abstract Object determineCurrentLookupKey();

resolvedDataSources is a HashMap < String, Object > resolvedDefaultDataSource is a DataSource

So the whole logic is very clear: AbstractRouting DataSource is an abstract class, we just need to inherit it, then provide a HashMap containing multiple data sources, and also provide a default data source resolvedDefaultDataSource, then implement determineCurrentLookupKey to return a String type key, through which we can find a corresponding data source. If not, use the default data source.

Next we will implement a dynamic data source by inheriting AbstractRouting Data Source.

Implementation of AbstractRouting Data Source

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 {
    // The same thread holds the same 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();
    }
}

In fact, there is no need to divide it into two categories at all. Although this can make the logic of DynamicDataSource clearer, it is not necessarily clearer for the whole.

Using ThreadLocal to hold a key for each thread is only a means, but it can also be implemented in other ways.

Let's take a look at the configuration file of the data source to match the configuration file with the 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}" />
        <!-- Configuration initialization size, minimum, maximum -->
        <property name="initialSize" value="10" />
        <property name="minIdle" value="10" />
        <property name="maxActive" value="60" />
        <!-- The maximum waiting time for a connection from the pool, in units ms -->
        <property name="maxWait" value="3000" />
        <!-- Configure the minimum lifetime of a connection in the pool in milliseconds -->
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <!-- How often is the configuration interval detected to detect idle connections that need to be closed in milliseconds? -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- Test statement -->
        <property name="validationQuery" value="SELECT 'x'" />
        <!-- Indicates whether the connection is idle connection Reclaimer(If so)Inspection -->
        <property name="testWhileIdle" value="true" />
        <!-- Is the connection checked before it is removed from the pool? -->
        <property name="testOnBorrow" value="false" />
        <!-- Is it checked before returning to the pool? -->
        <property name="testOnReturn" value="false" />
        <!-- open PSCacheļ¼ŒAnd specify on each connection PSCache Size -->
        <property name="poolPreparedStatements" value="false" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
        <!-- Configuration Monitoring Statistical Interception filters -->
        <property name="filters" value="stat" />
        <!-- open removeAbandoned function -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800 Seconds, or 30 minutes -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- Close abanded Output error log at connection time -->
        <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>

    <!-- Dynamic Data Source -->
    <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>
        <!-- Default data source -->
        <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>

    <!--  To configure mapper The mapping scanner automatically generates the mapping scanner based on the interface defined in the package dao Implementation class-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.freemethod.dao.mapper"/>
    </bean>

</beans>

Let's first look at the bean id for design, and notice that the bean is configured with abstract= "true" to indicate that the bean is not instantiated for inheritance by other beans. This bean uses com.alibaba.druid.pool.DruidDataSource.

Next, we configure two data sources, one master and one slave, which inherit design.

Focus on the bean s with id of dynamicDataSource. This is our inheritance of AbstractRouting DataSource. We see that two data sources master and slave are injected into Map with attribute of targetDataSources. key is also master and slave. The default data source defaultTargetDataSource is configured as master.

So in our determineCurrent Lookup Key of DynamicDataSource, if the master is returned, the master data source is used, and if the slave is returned, the slave data source is used.

All of the above mentioned are related to dynamic data sources and not used in AOP. Now let's introduce the application of AOP.

Switching Data Sources Using AOP

Since AOP is used, of course, there must be an aspect. Let's first look at the code of the aspect.

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();
    }
}

First, we need to identify the connection point, that is, where to switch the data source operation, here it is clear that we need to switch the data source on the method with the DataSourceKey annotation.

Based on the Pointcut expression we learned earlier, we can easily write the following expression:

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

Notification logic is also simple. We just need to set the thread's context key to the key of the data source obtained on the method annotation. After the method is executed, it is set to the previous data source. In this way, in the process of method execution, if the data source used gets the corresponding data source of the configuration on the method annotation.

Let's see how the following examples are used:

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

Test code:

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;
    }

}

For complete code, please download the link of the complete engineering code in the reference. The reason why the test class is posted here is that there is a very tangled problem to be addressed. You should not be able to search the relevant information. So if you're interested, you'd better download the source code and test it against it.

Careful students may have noticed the code annotated in the tangent class DataSourceAspect:

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

There are two contradictions. One is that the type injected in Spring can only be interface type, such as in test:

@Resource
UserService userServiceImpl;

If replaced by:

@Resource
UserServiceImpl userServiceImpl;

The injection fails.

But in Spring AOP (using AspectJ), the following code is used:

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

Instead of the actual delegation Method, that is to say, the actual type of UserService injection is UserService Impl, but the signature Method obtained on the Method invocation by UserService is the Method signature of UserService. What's the effect of this? The most direct impact in the above example is that annotations on Methods in userService Impl are not available to methods above the public.

I was stunned at first, and for a long time, I finally injected comments into the notification through the Advice parameter. As follows:

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

The Parameter for Advice can refer to the following article, Spring AOP III: Advice Method Parameters.

Reference resources

Project Code Cloud Link

Complete engineering code

One of Spring AOP: Basic concepts and processes

Spring AOP bis: Pointcut annotated expression

Spring AOP III: Advice method parameters

Posted by Shamrox on Mon, 17 Jun 2019 14:24:26 -0700