掘金 后端 ( ) • 2024-05-14 17:34

theme: cyanosis

前言

上一篇文章在SelectProvider获取需要操作的bean对象时,出现了点问题,导致泛型对象<T>需要传入到dao层的每个方法,十分不合理。后面参考了下网上的教程,跟着用上了AOP进行切面编程,切面方法将当前dao的内容获取到ThreadLocal中,数据操作方法执行时获取dao内容。

image.png

AOP工作原理图

正文

各模块一览

name description nott-mybatis-curd mybaits-基础 nott-mybatis-dynamic-datasource mybatis-动态数据源 nott-web-test web测试模块

完整项目文件见GITHUB代码仓库,如果对你有用请点个star。

引入依赖

在根目录中引入需要的ASPECT包。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.21.1</version>
    <scope>runtime</scope>
</dependency>

Pointcut配置

这里我们需要'拦截'这个基础CommonMapper下的方法,包路径为org.nott.mybatis.mapper,所以aop表达式应为execution(* org.nott.mybatis.mapper.*.*(..)),表示Pointcut包括org.nott.mybatis.mapper下的所有方法,它下面的某个方法就是JoinPoint。然后在执行方法前将它的当前泛型放入到需要的地方,BaseSelectProvider执行时获取泛型内容并实例化bean。 image.png CommonMapper

创建在curd模块包下aop相关的创建配置类,用于读取aop的表达式。

@Data
@Component
@ConfigurationProperties("nott.mybatis.aop")
public class MybatisAopConfig {

    /**
     * 拦截基础的CommonMapper aop表达式
     */
    private String baseAopPackageExpression = "execution(* org.nott.mybatis.mapper.*.*(..))";

    /**
     * 自定义加上的aop表达式
     */
    private String[] appendAopPackageExpression;

}

创建信息储存对象

我们aop的PointCut已经定义完成,需要定义每一个方法执行前都存储的信息对象,存放当前mapper的类、执行方法、参数等。

@Data
public class ExecuteMapperContextBean {

    /**
     * 当前mapper的class
     */
    private Class<?> currentMapperClass;

    /**
     * 执行的方法
     */
    private Method executeMethod;

    /**
     * 参数
     */
    private Parameter[] parameters;


}

Advice

至此,已经定义好需要获取信息的方法和用来存储的对象,接下来就定义怎么获取方法的内容。

创建继承至MethodInterceptor的MybatisAopInterceptor,获取当前执行的方法,在把它转成ExecuteMapperContextBean存储到ThreadLocal<ExecuteMapperContextBean>里面。

@Component
public class MybatisAopInterceptor implements MethodInterceptor {

    private static final ThreadLocal<ExecuteMapperContextBean> mapperContextThreadLocal = new ThreadLocal<>();

    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 {
        // 存储信息
        this.set(invocation);

        Object result = null;
        try {
            // JoinPoint方法执行
            result = invocation.proceed();
            return result;
        } catch (Exception e) {
            throw  e;
        } finally {
            // 最后,释放mapperContextThreadLocal中信息
            mapperContextThreadLocal.remove();
        }
    }

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

这样,已经定义好存储dao层方法执行时的信息的行为动作,和它执行的时机,就剩下把整套动作行为拼在一起了。

PointcutAdvisor

Advisor是aop管理Advice和PointCut的顶级接口,它的部分继承关系如图所示。

image.png

这里我们需要设置Advice和PointCut的属性,因此需要定义AspectJExpressionPointcutAdvisor的bean。

@Configuration
@EnableConfigurationProperties(MybatisAopConfig.class)
@RequiredArgsConstructor
public class MapperContextAutoConfiguration {

    private final MybatisAopConfig mybatisAopConfig;

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

    @Bean
    public PointcutAdvisor setMyBatisPointcutAdvisor(){
        // 设置拦截表达式(PointCut)
        AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
        String baseAopPackage = mybatisAopConfig.getBaseAopPackageExpression();
        advisor.setExpression(baseAopPackage);
        String[] appendAopPackage = mybatisAopConfig.getAppendAopPackageExpression();
        if (appendAopPackage != null && appendAopPackage.length > 0) {
            for (String packageExpression : appendAopPackage) {
                advisor.setExpression(packageExpression);
            }
        }

        // 执行动作
        advisor.setAdvice(setMybatisAopInterceptor());

        return advisor;

    }
}

可以注意到MybatisAopInterceptor作为setAdvice(Advice advice)方法的参数,而它是实现MethodInterceptor接口的。关于Interceptor为什么能作为setAdvice的参数,这个关系图可以说明。

image.png

验证

在web模块,创建UserMapper继承作为JoinPoint的CommonMapper,CommonMapper定义了一个selectList的通用查询方法

public interface UserMapper extends CommonMapper<User> {
public interface CommonMapper<T> {

    @SelectProvider(type = BaseSelectProvider.class,method = "selectList")
    public List<T> selectList();

}

在MybatisAopInterceptor中打上断点,如果正确执行,程序会在此卡住,此时set方法已经把信息放入ThreadLocal中,debug追踪可以看到目前执行方法的信息。

image.png

编写测试类

image.png

断点

image.png

小结

纸上得来终觉浅 绝知此事要躬行

之前都是在网上看有关AOP的教程,看得一头雾水,内容很是抽象。等到实际运用的时候,才感觉里面设计的十分巧妙,有很多门道可以学习使用,实际运用其中我也看了很多网上的相关教程,学习了很多意想不到的东西。