Spring AOP---深入剖析AOP注解实现原理

前言

阅读本文之前建议先掌握的前置知识:

1.概述

Spring AOP有常用的两种方式,一种是使用XML的方式,另一种是使用注解的方式。本文将详细的分析注解方式的实现原理。将会从如下几个点展开。

  • Spring如何集成AspectJ AOP
  • AOP通知链如何形成
  • 何时进行AOP动态代理以及动态代理的方式
  • 通知链的调用过程

用于调试的代码如下:

代码远程仓库地址:https://gitee.com/gongsenlin/aoptest/tree/master

//启动类
package hdu.gongsenlin.aoptest;

import hdu.gongsenlin.aoptest.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AoptestApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
        // jdk
//        MyService a = (MyService) ac.getBean("userService");
//        a.query();
        //cglib
        UserService a = ac.getBean(UserService.class);
        a.query();
    }

}
//配置类
package hdu.gongsenlin.aoptest;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

/**
 * @author gongsenlin
 * @version 1.0
 * @date 2021-4-22 19:59
 */
@Configuration
@ComponentScan("hdu.gongsenlin.aoptest")
@EnableAspectJAutoProxy
public class Appconfig {
}

//切面类1
package hdu.gongsenlin.aoptest.aop;

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;
import org.springframework.core.Ordered;

/**
 * @author gongsenlin
 * @version 1.0
 * @date 2021-4-22 20:00
 */
@Aspect
@Component
public class Aop{


    @Pointcut("execution(* hdu.gongsenlin.aoptest.service..*.*(..))")
    private void pointcut(){}

    @Pointcut("execution(* hdu.gongsenlin.aoptest.controller..*.*(..))")
    private void pointcut2(){}

    @Before("pointcut()")
    public void beforeAdvice(){
        System.out.println("前置增强1");
    }

    @Before("pointcut()")
    public void abeforeAdvice(){
        System.out.println("前置增强2");
    }

    @After("pointcut()")
    public void afterAdvice(){
        System.out.println("后置增强1");
    }

    @After("pointcut2()")
    public void afterAdvice2(){
        System.out.println("后置增强2");
    }

}

//业务类
package hdu.gongsenlin.aoptest.service;

import org.springframework.stereotype.Component;

/**
 * @author gongsenlin
 * @version 1.0
 * @date 2021-4-22 19:59
 */
@Component
public class UserService implements MyService {

    @Override
    public void query(){
        System.out.println("query do");
    }

    @Override
    public void f() {

    }
}

public interface MyService {

    void f();

    void query();
}

2.Spring如何集成AspectJ AOP

主要是@EnableAspectJAutoProxy注解的作用。

该注解是一个复合注解,如下所示:
在这里插入图片描述
@Import注解可以注入一个类到Spring容器当中,在之前的博客中详细介绍过该注解的作用,博客地址如下:

《Spring中@Import注解的使用和实现原理》

ConfigurationClassPostProcessor解析启动时候的配置类Appconfig

由于Appconfig的注解中包含了@Import注解,那么会将@Import注解中的类 先缓存到Appconfig配置类的importBeanDefinitionRegistrars集合中。
在这里插入图片描述
ConfigurationClassPostProcessor扫描完了所有的类,执行加载和注册beanDefinition的时候

此时如果该类中包含了ImportBeanDefinitionRegistrar

那么会调用该类的registerBeanDefinitions方法进行注册beanDefinition。

也就来到了AspectJAutoProxyRegistrar中的registerBeanDefinitions方法。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

该方法的作用就是将AnnotationAwareAspectJAutoProxyCreator注册到容器当中,这个类是Spring AOP的关键。

此时Spring中就有了Aspect AOP的功能

Spring是需要手动添加@EnableAspectJAutoProxy注解进行集成的,而SpringBoot中使用自动装配的技术,可以不手动加这个注解就实现集成。

在org/springframework/boot/spring-boot-autoconfigure/2.1.7.RELEASE/spring-boot-autoconfigure-2.1.7.RELEASE.jar!/META-INF/spring.factories中,添加了AopAutoConfiguration,AopAutoConfiguration中加上了@EnableAspectJAutoProxy注解。

有了AopAutoConfiguration配置类,后续的解析配置类的逻辑就和Spring是一样的了。

SpringBoot如何实现自动注入,可以参考博客《SpringBoot自动装配原理分析和实现自定义启动器》
在这里插入图片描述
在这里插入图片描述

3.AOP通知链如何生成

了解Bean的生命周期的读者,一定清楚BeanPostProcessor接口的postProcessAfterInitialization方法。

AspectJAwareAdvisorAutoProxyCreator实现了BeanPostProcessor该接口,所以在每个Bean的生命周期中,都会执行AspectJAwareAdvisorAutoProxyCreator的postProcessAfterInitialization方法。
在这里插入图片描述
在这里插入图片描述
来到父类中AbstractAutoProxyCreator中的postProcessAfterInitialization。只有当earlyProxyReferences集合中不存在cacheKey的时候,才会执行wrapIfNecessary方法。该集合的作用在之前的博客中也说明了。

SpringAOP对象生成的时机有两个,一个是提前AOP,提前AOP的对象会被放入到earlyProxyReferences集合当中,若没有提前AOP那么会在Bean的生命周期的最后执行postProcessAfterInitialization的时候进行AOP动态代理。这一部分知识不清楚的读者可以阅读博客《Spring IOC—AOP代理对象生成的时机》

没有进行AOP的对象会执行wrapIfNecessary方法,该方法中会去寻找通知链,若存在匹配的通知链,那么会进行AOP动态代理。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
调用findCandidateAdvisors方法 得到所有的候选通知

然后调用findAdvisorsThatCanApply方法进行过滤,判断是否可以作用于当前Bean的增强处理。
在这里插入图片描述
调用父类的findCandidateAdvisors方法是加载使用XML方式配置的AOP声明,此处不讨论。

下面会调用aspectJAdvisorsBuilder的buildAspectJAdvisors方法去加载注解方式配置的AOP声明。
在这里插入图片描述
在这里插入图片描述
第一次进来的时候,此时缓存还是为空的,那么会进第一个if,从容器中拿到所有的beanName。

遍历BeanName,得到其对应的对象类型,调用 this.advisorFactory.isAspect(beanType)判断该类是否是切面类,也就是判断是否加了@Aspect注解。

在这里插入图片描述
是切面类的话,判断切面类的PerClause是否是Singleton,正常来说是的,接着构建用于生成通知链的工厂对象。然后基于该工厂对象生产通知链,调用this.advisorFactory.getAdvisors(factory)方法
在这里插入图片描述
getAdvisorMethods 获得类中所有的方法(包含父类的方法),pointCut方法除外。

遍历这些方法,判断是否是通知,构建通知并添加到通知链当中。

解析切面类中的成员变量,判断是否加了@DeclareParents注解,构建DeclareParentsAdvisor。

每个advisors中都包含了PointCut切点相关的内容。

测试代码得到的通知链结果如下:
在这里插入图片描述
回到BeanFactoryAspectJAdvisorsBuilder中的buildAspectJAdvisors方法。
在这里插入图片描述
如果这个切面bean是一个单例,那么将得到的通知链放入advisorsCache缓存当中,beanName作为key,通知链作为value。如果切面bean不是单例的,那么将用于构建通知链的工厂放入aspectFactoryCache缓存当中,beanName作为key,构建通知链的工厂作为value。

之后再访问BeanFactoryAspectJAdvisorsBuilder中的buildAspectJAdvisors方法的时候,直接根据beanName拿到对应的缓存即可。

目前拿到的通知链是还没有经历过匹配筛选的,方法来到AbstractAdvisorAutoProxyCreator中的findEligibleAdvisors方法
在这里插入图片描述
findCandidateAdvisors()方法返回的是候选的通知链,下面需要对里面的通知进行筛选,可以匹配上切点定义的通知才可以作用到当前的Bean上。

调用findAdvisorsThatCanApply方法进行通知链的匹配,匹配规则就根据切点中的execution表达式
在这里插入图片描述
候选通知原本有4个,筛选后,满足条件的仅有3个。其中afterAdvice2方法描述的通知所对应的切点表达式和当前加载的bean匹配不上。
在这里插入图片描述
筛选完之后,在调用extendAdvisors 添加一个ExposeInvocationInterceptor拦截器,用于再通知链的任意一环得到MethodInvocation对象。具体的作用下面会说明。

最后对通知进行排序,至此就得到了用于增强的通知链。

以上就是通知链的构建过程。简单来说就是遍历所有的类,判断类上是否加了@Aspect注解,对加了该注解的类再判断其拥有的所有方法,对于加了通知注解的方法构建出Advisor通知对象放入候选通知链当中。接着基于当前加载的Bean筛选通知,添加ExposeInvocationInterceptor拦截器,最后对通知链进行排序,得到最终的通知链。测试代码得到的最终通知链结果如下:
在这里插入图片描述

4.何时进行AOP动态代理以及动态代理的方式

上一节中得到了用于增强的通知链后,代码定位到AbstractAutoProxyCreator中的wrapIfNecessary方法
在这里插入图片描述
第一个红色框框得到的结果是完整的通知链,第二个红色框框就是进行动态代理的地方了。下面来看看createProxy做了些什么。
在这里插入图片描述
构建ProxyFactory代理工厂,判断使用JDK动态代理还是使用CGLib动态代理

设置代理工厂的一些属性,例如通知链,被代理对象等。

然后调用工厂的getProxy方法得到代理对象。
在这里插入图片描述
基于工厂属性的设置,调用createAopProxy()得到不同的对象。
在这里插入图片描述
AOP的代理方式有两种,一种是Cglib代理,使用ObjenesisCglibAopProxy来创建代理对象,另一种是JDK动态代理,使用JdkDynamicAopProxy来创建代理对象。

都是调用其getProxy方法。

  • 使用ObjenesisCglibAopProxy创建代理对象

    ObjenesisCglibAopProxy继承自CglibAopProxy,getProxy方法来自父类,如下
    在这里插入图片描述
    构建Enhancer对象,设置属性。我们比较关心的通知链也被设置在Enhancer对象当中。
    在这里插入图片描述
    最后调用createProxyClassAndInstance得到基于Cglib动态代理的对象。

  • 使用JdkDynamicAopProxy创建代理对象

    此时修改一下测试代码中的UserService,让它实现一个接口。这样就会走JDK的动态代理。重新调试

    getProxy代码如下: ​在这里插入图片描述
    这就是比较熟悉的JDK动态代理的方式了。JdkDynamicAopProxy本身也实现了InvocationHandler。所以这里newProxyInstance第三个参数传入的是自身。下一节再来看具体的调用链的执行过程。

5.通知链的调用过程

通过java自带的工具HSDB,可以查看动态生成的类的源代码。

  • JDK动态代理生成的类
    在这里插入图片描述
    在这里插入图片描述
    query方法内调用了父类中的InvocationHandler的invoke,也就是调用了JdkDynamicAopProxy中的invoke方法。JdkDynamicAopProxy实现了InvocationHandler接口。

    invoke方法如下:
    在这里插入图片描述
    在这里插入图片描述

  1. SpringAOP不会对equals和hashCode方法进行增强,除非这两个方法是定义在接口中的。
  2. 这两个还没有理解,清楚的读者可以分享一下,感谢!在这里插入图片描述
  3. 判断是否暴露代理对象,默认是false可以设置为true,如果暴露的话,那么在同一个线程中的任意地方都可以通过AopContext获取该代理对象。
  4. 获取方法对应的通知链,此时的通知链应该说是拦截器链,对于原来的Advisor都装饰成了MethodInterceptor。如果为空,那么直接反射调用目标方法,如果不为空,那么会构建一个MethodInvocation对象,调用该对象的proceed()方法执行拦截器链以及目标方法的调用过程。
  • CGLib生成的代理类如下:
    在这里插入图片描述
    在这里插入图片描述
    query方法内调用CGLIB C A L L B A C K 0 的 i n t e r c e p t 方 法 , C G L I B CALLBACK_0的intercept方法,CGLIB CALLBACK0interceptCGLIBCALLBACK_0如下
    在这里插入图片描述
    也就是调用了DynamicAdvisedInterceptor的Intercept
    在这里插入图片描述
    和jdk的动态代理类似,最后都会构建一个MethodInvocation 然后调用它的proceed()方法。
    在这里插入图片描述
    无论是JDK还是CGLib的方式,都会调用MethodInvocation的proceed方法,首先回来代码会来到ReflectiveMethodInvocation中的proceed。该类中维护了一个索引currentInterceptorIndex,用来表示当前访问的是第几个拦截器。
    第一个if是判断所有的拦截器是否都已经访问完,如果是的话,则反射调用目标方法。
    基于currentInterceptorIndex索引拿到当前的拦截器 判断拦截器的类型选择调用拦截器的invoke方法。
    首先来看一下现在的拦截器链的组成
    在这里插入图片描述
    第一个是用于暴露MethodInvocation用的,将MethodInvocation放入ThreadLocal中,同一线程的任何地方都可以得到。
    ExposeInvocationInterceptor的invoke方法如下:
    在这里插入图片描述
    暴露MethodInvocation然后调用MethodInvocation的proceed(),也就又回到了
    在这里插入图片描述
    此时的索引比刚才多了1,拿到了第二个拦截器
    第二个拦截器是MethodBeforeAdviceInterceptor,里面包含了MethodBeforeAdvice前置通知。之所以这样做,是起到了适配器的作用,外部调用都是通过invoke方法即可。
    invoke方法如下:
    在这里插入图片描述
    在这里插入图片描述
    内部调用了前置通知的before方法,完成前置通知的逻辑。
    接着还是调用MethodInvocation的proceed方法。
    currentInterceptorIndex索引下标拿到第三个,第三个也是前置通知,这里就不看了。第四个是后置通知
    在这里插入图片描述
    后置通知的话,先执行MethodInvocation的proceed方法,然后再进行该通知的逻辑。
    遍历完所有的后置通知,就会执行目标的方法,然后再回过来执行finally代码块中的后置通知的逻辑。

6.后续

下一篇博客将探究SpringAOP使用时的注意事项。

例如:同一个切面类中的多个同类型的通知的排列顺序,不同切面类中的同类型的通知的排列顺序等。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值