Record of Implementing Universal CRUD Functions for Mybatis-CommonMapper (Part 3 - Introducing Programmatic AOP)


Topic: Cyanosis

Preface

There was an issue when obtaining the bean object to operate on using SelectProvider in the previous article, which made it necessary to pass the generic object to every method in the DAO layer, which was highly unreasonable. Later, I referred to online tutorials and used AOP for aspect-oriented programming. The aspect method retrieves the current DAO content into ThreadLocal, and the data operation method gets the DAO content when executing.

AOP Working Principle Diagram

Main Content

Module Overview

Name Description
nott-mybatis-curd MyBatis Basics
nott-mybatis-dynamic-datasource MyBatis Dynamic Data Source
nott-web-test Web Test Module

The complete project files can be found in the GITHUB repository. Please leave a star if it's helpful to you.

Import Dependencies

Import the required ASPECT package in the root directory.

<dependency>
    <groupid>org.aspectj</groupid>
    <artifactid>aspectjweaver</artifactid>
    <version>1.9.21.1</version>
    <scope>runtime</scope>
</dependency>

Pointcut Configuration

Here we need to 'intercept' the methods under the base CommonMapper, whose package path is org.nott.mybatis.mapper, so the AOP expression should be execution(* org.nott.mybatis.mapper.*.*(..)), which means the Pointcut includes all methods under org.nott.mybatis.mapper, and any method under it is a JoinPoint. Then, before executing the method, put its current generic type into the required place, and the BaseSelectProvider will obtain the generic content and instantiate the bean when executing.

CommonMapper

Create an AOP-related configuration class under the curd module package to read the AOP expressions.

@Data
@Component
@ConfigurationProperties("nott.mybatis.aop")
public class MybatisAopConfig {
/**
 * Intercept base CommonMapper AOP expression
 */
private String baseAopPackageExpression = "execution(* org.nott.mybatis.mapper.*.*(..))";

/**
 * Custom added AOP expressions
 */
private String[] appendAopPackageExpression;

}

Create Information Storage Object

Our AOP Pointcut has been defined, now we need to define an information object that is stored before each method executes, to store the current mapper's class, executed method, parameters, etc.

@Data
public class ExecuteMapperContextBean {
/**
 * Current mapper's class
 */
private Class<!--?--> currentMapperClass;

/**
 * Executed method
 */
private Method executeMethod;

/**
 * Parameters
 */
private Parameter[] parameters;

}

Advice

So far, we have defined the method to obtain information and the object used for storage, next we will define how to get the method's content.

Create MybatisAopInterceptor that inherits from MethodInterceptor, get the currently executing method, convert it to ExecuteMapperContextBean and store it in ThreadLocal.

@Component
public class MybatisAopInterceptor implements MethodInterceptor {
private static final ThreadLocal<executemappercontextbean> mapperContextThreadLocal = new ThreadLocal&lt;&gt;();

public void set(MethodInvocation invocation){
    ExecuteMapperContextBean bean = new ExecuteMapperContextBean();

    Class<!--?--> aClass = Objects.requireNonNull(invocation.getThis()).getClass();

    Class<!--?-->[] interfacesForClass = ClassUtils.getAllInterfacesForClass(aClass, aClass.getClassLoader());

    Class<!--?-->[] interfaces = interfacesForClass[0].getInterfaces();

    bean.setExtendMapperClass(interfaces[0]);

    bean.setCurrentMapperClass(interfacesForClass[0]);

    bean.setParameters(invocation.getMethod().getParameters());

    bean.setExecuteMethod(invocation.getMethod());

    mapperContextThreadLocal.set(bean);
}

@Nullable
@Override
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
    // Store information
    this.set(invocation);

    Object result = null;
    try {
        // Execute JoinPoint method
        result = invocation.proceed();
        return result;
    } catch (Exception e) {
        throw  e;
    } finally {
        // Finally, clear the information in mapperContextThreadLocal
        mapperContextThreadLocal.remove();
    }
}

public static ThreadLocal<executemappercontextbean> getContext(){
    return mapperContextThreadLocal;
}

}

In this way, we have defined the behavior of storing information when the DAO layer method executes, and its execution timing, now we just need to put the entire set of behaviors together.

PointcutAdvisor

Advisor is the top-level interface for AOP to manage Advice and Pointcut, its partial inheritance relationship is shown in the figure.

Here we need to set the properties of Advice and Pointcut, so we need to define the bean for AspectJExpressionPointcutAdvisor.

@Configuration
@EnableConfigurationProperties(MybatisAopConfig.class)
@RequiredArgsConstructor
public class MapperContextAutoConfiguration {
private final MybatisAopConfig mybatisAopConfig;

@Bean
public Interceptor setMybatisAopInterceptor(){
    return new MybatisAopInterceptor();
}

@Bean
public PointcutAdvisor setMyBatisPointcutAdvisor(){
    // Set interception expression (Pointcut)
    AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
    String baseAopPackage = mybatisAopConfig.getBaseAopPackageExpression();
    advisor.setExpression(baseAopPackage);
    String[] appendAopPackage = mybatisAopConfig.getAppendAopPackageExpression();
    if (appendAopPackage != null &amp;&amp; appendAopPackage.length &gt; 0) {
        for (String packageExpression : appendAopPackage) {
            advisor.setExpression(packageExpression);
        }
    }

    // Execute action
    advisor.setAdvice(setMybatisAopInterceptor());

    return advisor;

}

}

You may notice that MybatisAopInterceptor is used as the parameter of the setAdvice(Advice advice) method, and it implements the MethodInterceptor interface. This diagram can explain why the Interceptor can be used as the parameter of setAdvice.

Verification

In the web module, create UserMapper that inherits from CommonMapper which acts as the JoinPoint. CommonMapper defines a general query method selectList

public interface UserMapper extends CommonMapper {
public interface CommonMapper {
@SelectProvider(type = BaseSelectProvider.class,method = "selectList")
public List<t> selectList();

}

Set a breakpoint in MybatisAopInterceptor. If the execution is correct, the program will pause here. At this point, the set method has already put the information into ThreadLocal, and you can see the currently executed method's information via debug tracing.

Write test class

Breakpoint

Summary

What you learn from books is superficial; you must practice it to understand it thoroughly.

I used to watch AOP tutorials online and was completely confused, the content was very abstract. When I actually used it, I realized that the design is very clever, with many tricks worth learning. During actual use, I also referred to many relevant online tutorials and learned many unexpected things.


This is a discussion topic separated from the original topic at https://juejin.cn/post/7368669650576867337