文章目录

  • 前言
  • 1.概述
  • 2.Spring如何集成AspectJ AOP
  • 3.AOP通知链如何生成
  • 4.何时进行AOP动态代理以及动态代理的方式
  • 5.通知链的调用过程
  • 6.后续


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注解的作用。

该注解是一个复合注解,如下所示:

spring注解校验是否为数字 spring 注解实现原理_aop


@Import注解可以注入一个类到Spring容器当中,在之前的博客中详细介绍过该注解的作用,博客地址如下:

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

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

由于Appconfig的注解中包含了@Import注解,那么会将@Import注解中的类 先缓存到Appconfig配置类的importBeanDefinitionRegistrars集合中。

spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_02


ConfigurationClassPostProcessor扫描完了所有的类,执行加载和注册beanDefinition的时候

此时如果该类中包含了ImportBeanDefinitionRegistrar

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

也就来到了AspectJAutoProxyRegistrar中的registerBeanDefinitions方法。

spring注解校验是否为数字 spring 注解实现原理_java_03


spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_04


spring注解校验是否为数字 spring 注解实现原理_spring_05

该方法的作用就是将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自动装配原理分析和实现自定义启动器》

spring注解校验是否为数字 spring 注解实现原理_java_06


spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_07

3.AOP通知链如何生成

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

AspectJAwareAdvisorAutoProxyCreator实现了BeanPostProcessor该接口,所以在每个Bean的生命周期中,都会执行AspectJAwareAdvisorAutoProxyCreator的postProcessAfterInitialization方法。

spring注解校验是否为数字 spring 注解实现原理_AOP_08


spring注解校验是否为数字 spring 注解实现原理_AOP_09


来到父类中AbstractAutoProxyCreator中的postProcessAfterInitialization。只有当earlyProxyReferences集合中不存在cacheKey的时候,才会执行wrapIfNecessary方法。该集合的作用在之前的博客中也说明了。

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

没有进行AOP的对象会执行wrapIfNecessary方法,该方法中会去寻找通知链,若存在匹配的通知链,那么会进行AOP动态代理。

spring注解校验是否为数字 spring 注解实现原理_aop_10


spring注解校验是否为数字 spring 注解实现原理_java_11


spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_12


调用findCandidateAdvisors方法 得到所有的候选通知然后调用findAdvisorsThatCanApply方法进行过滤,判断是否可以作用于当前Bean的增强处理。

spring注解校验是否为数字 spring 注解实现原理_AOP_13


调用父类的findCandidateAdvisors方法是加载使用XML方式配置的AOP声明,此处不讨论。下面会调用aspectJAdvisorsBuilder的buildAspectJAdvisors方法去加载注解方式配置的AOP声明。

spring注解校验是否为数字 spring 注解实现原理_java_14


spring注解校验是否为数字 spring 注解实现原理_spring_15


第一次进来的时候,此时缓存还是为空的,那么会进第一个if,从容器中拿到所有的beanName。

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

spring注解校验是否为数字 spring 注解实现原理_AOP_16


是切面类的话,判断切面类的PerClause是否是Singleton,正常来说是的,接着构建用于生成通知链的工厂对象。然后基于该工厂对象生产通知链,调用this.advisorFactory.getAdvisors(factory)方法

spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_17


getAdvisorMethods 获得类中所有的方法(包含父类的方法),pointCut方法除外。

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

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

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

测试代码得到的通知链结果如下:

spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_18


回到BeanFactoryAspectJAdvisorsBuilder中的buildAspectJAdvisors方法。

spring注解校验是否为数字 spring 注解实现原理_aop_19


如果这个切面bean是一个单例,那么将得到的通知链放入advisorsCache缓存当中,beanName作为key,通知链作为value。如果切面bean不是单例的,那么将用于构建通知链的工厂放入aspectFactoryCache缓存当中,beanName作为key,构建通知链的工厂作为value。

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

目前拿到的通知链是还没有经历过匹配筛选的,方法来到AbstractAdvisorAutoProxyCreator中的findEligibleAdvisors方法

spring注解校验是否为数字 spring 注解实现原理_aop_20


findCandidateAdvisors()方法返回的是候选的通知链,下面需要对里面的通知进行筛选,可以匹配上切点定义的通知才可以作用到当前的Bean上。调用findAdvisorsThatCanApply方法进行通知链的匹配,匹配规则就根据切点中的execution表达式

spring注解校验是否为数字 spring 注解实现原理_AOP_21


候选通知原本有4个,筛选后,满足条件的仅有3个。其中afterAdvice2方法描述的通知所对应的切点表达式和当前加载的bean匹配不上。

spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_22


筛选完之后,在调用extendAdvisors 添加一个ExposeInvocationInterceptor拦截器,用于再通知链的任意一环得到MethodInvocation对象。具体的作用下面会说明。

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

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

spring注解校验是否为数字 spring 注解实现原理_spring_23

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

上一节中得到了用于增强的通知链后,代码定位到AbstractAutoProxyCreator中的wrapIfNecessary方法

spring注解校验是否为数字 spring 注解实现原理_java_24


第一个红色框框得到的结果是完整的通知链,第二个红色框框就是进行动态代理的地方了。下面来看看createProxy做了些什么。

spring注解校验是否为数字 spring 注解实现原理_java_25


构建ProxyFactory代理工厂,判断使用JDK动态代理还是使用CGLib动态代理

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

然后调用工厂的getProxy方法得到代理对象。

spring注解校验是否为数字 spring 注解实现原理_AOP_26


基于工厂属性的设置,调用createAopProxy()得到不同的对象。

spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_27


AOP的代理方式有两种,一种是Cglib代理,使用ObjenesisCglibAopProxy来创建代理对象,另一种是JDK动态代理,使用JdkDynamicAopProxy来创建代理对象。

都是调用其getProxy方法。

  • 使用ObjenesisCglibAopProxy创建代理对象
    ObjenesisCglibAopProxy继承自CglibAopProxy,getProxy方法来自父类,如下
  • spring注解校验是否为数字 spring 注解实现原理_java_28

  • 构建Enhancer对象,设置属性。我们比较关心的通知链也被设置在Enhancer对象当中。
  • spring注解校验是否为数字 spring 注解实现原理_spring_29

  • 最后调用createProxyClassAndInstance得到基于Cglib动态代理的对象。
  • 使用JdkDynamicAopProxy创建代理对象
    此时修改一下测试代码中的UserService,让它实现一个接口。这样就会走JDK的动态代理。重新调试
    getProxy代码如下:
  • spring注解校验是否为数字 spring 注解实现原理_AOP_30

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

5.通知链的调用过程

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

  • JDK动态代理生成的类
  • spring注解校验是否为数字 spring 注解实现原理_aop_31


  • spring注解校验是否为数字 spring 注解实现原理_spring注解校验是否为数字_32

  • query方法内调用了父类中的InvocationHandler的invoke,也就是调用了JdkDynamicAopProxy中的invoke方法。JdkDynamicAopProxy实现了InvocationHandler接口。
    invoke方法如下:
  • spring注解校验是否为数字 spring 注解实现原理_AOP_33


  • spring注解校验是否为数字 spring 注解实现原理_spring_34

  1. SpringAOP不会对equals和hashCode方法进行增强,除非这两个方法是定义在接口中的。
  2. 这两个还没有理解,清楚的读者可以分享一下,感谢!
  3. 判断是否暴露代理对象,默认是false可以设置为true,如果暴露的话,那么在同一个线程中的任意地方都可以通过AopContext获取该代理对象。
  4. 获取方法对应的通知链,此时的通知链应该说是拦截器链,对于原来的Advisor都装饰成了MethodInterceptor。如果为空,那么直接反射调用目标方法,如果不为空,那么会构建一个MethodInvocation对象,调用该对象的proceed()方法执行拦截器链以及目标方法的调用过程。
  • CGLib生成的代理类如下:


    query方法内调用CGLIBspring注解校验是否为数字 spring 注解实现原理_AOP_35CALLBACK_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使用时的注意事项。

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