Spring AOP 执行流程及源码分析(下)

简介: Spring AOP 执行流程及源码分析(下)

动态代理对象创建过程

AbstractAutoProxyCreator#postProcessAfterInitialization->wrapIfNecessary->createProxy

前言

Spring 创建代理对象只是在原有基础上作了一些额外的扩展,使用到了一个新的接口:Advised;proxyFactory 只是其中的一个实现 > AOP 中的一个核心类,它组合了 advisor、targetSource(目标对象)

  • advisor:通知器包含 advice、pointCut
  • advice:具体的某一个消息通知
  • advised:用来配置代理 > proxyFactory

创建过程

CGLIB 生成动态代理对象过程

CGLIB 生成动态代理对象过程,如下:

  1. 首先,通过当前类是否有实现接口以及通过 proxyTargetClass 属性值是否为 true,如果目标类实现了接口采用的是 JDK 动态代理实现,否则就采用 CGLIB 动态代理实现
  2. Advised 接口实现类 ProxyFactory,它包揽了目标类以及所有 advisor 通知的实现
  3. validateClassIfNecessary 用于校验类是否可被代理,final、static 修饰的方法不可被代理
  4. 创建 CGLIB 代理对象时,底层实现流程主要的几个对象、属性:ClassLoaderData(管理类加载的数据对象)、通过 Generator#generateClass 方法生成类的键值(键:EnhanceKey、value:存放的是生成 Enhancer 代理对象回调接口的实现)

前置对象准备好以后,就到了生成目标类的代理对象,先设置好扩展的一些属性

1、Spring 自带的前缀策略生成器,主要是为代理对象取名时追加的前缀-BySpringCGLIB

2、设置自定义的生成字节码策略器(内部实现主要是设置上下文对象,具体的生成还是调用了默认的策略生成器)

重要部分:设置好代理对象的一些拦截器回调实现类,存在七个实现 MethodInterceptor 接口的回调类型,DynamicAdvisedInterceptor 类才是最重要的入口

  1. 判断 exposeProxy 属性值是否为 true,如果是则需要将当前的代理对象设置为 ThreadLocal 线程本地变量模式,使其能够在上下文进行调用.

Spring 配合 ,在 SpringBoot 需要配合 @EnableAspectJAutoProxy(exposeProxy=true) 使用;比如在 ProxyClazz 类 A、B 方法中进行调用时事务会失效的情况下,就是因为这个类实现了代理,若直接相互调用,事务就会失效,若要确保它不失效:前者先要进行配置 exposeProxy 属性为 true,后者在方法期间通过 (ProxyClazz)AopContext.getProxy().B() 这种方式调用才能确保事务是有效的.

以下源码是否有 exposeProxy 属性时,设置目标拦截器的区别

// 源码方法:CglibAopProxy#getCallbacks
if (exposeProxy) {
// 区别目标源对象的 isStatic 方法返回值为真或假
  targetInterceptor = (isStatic ?
      new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) :
      new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()));
} else {
  targetInterceptor = (isStatic ?
      new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) :
      new DynamicUnadvisedInterceptor(this.advised.getTargetSource()));
}

通过 Debug 断点获取到的拦截器链以及创建方法执行的旅程 > 重要的入口:wrapIfNecessary,如下图:

JDK 生成动态代理对象过程

JDK 生成动态代理对象过程,如下:

  1. 获取目标类上的接口并且判断是否需要添加 SpringProxy、Advised、DecoratingProxy 接口
  2. 判断获取到的接口是否定义了 equals、hashCode 方法,一般是没有定义的,若有定义 equals 方法,标识 equalsDefined 为 true;若有定义 hashCode 方法,标识 hashCodeDefined 为 true
  3. 设置好接口和属性以后,创建我们的代理对象,Proxy.newProxyInstance(classLoader, proxiedInterfaces, this),基本的三件套

第一个参数是类加载器

第二个参数是在第一步获取到的所有接口

第三个参数是当前的 JdkDynamicAopProxy 类,它实现了 InvocationHandler 接口,后续在它的 invoke 方法调用处理.

动态代理执行流程

CGLIB 执行流程

如下图,AOP 拦截器对代码进行增强处理的详细执行流程图

上图中虽然能看出 Advice 执行的具体顺序,但是根据你实际的配置情况,顺序可能有所不同,所以要根据你的业务代码来判断具体要先执行那一个

当生成代理对象之后,会进行具体的逻辑方法调用,此时,AOP 若是 5 个增强通知都能适配的话,一共会有 6 个 Advisor,除了五种增强通知以外,还有一个是 ExposeInvocationInterceptor,它起着一个桥梁的作用,会将当前 MethodInvocation 对象存入线程上下文对象,它们在执行时是按照某个顺序来执行的,而且是由一个通知跳转到另外一个通知执行;所以此时,我们需要构建一个拦截器链条(责任链模式)只有创建好链式结构,才能顺利的向下执行。

1、首先它会通过切入点表达式,先进行类、方法匹配,获取到匹配的通知之后,会判别当前通知是否实现了 MethodInterceptor 接口,若没有实现的话,它会先作一层适配

2、前置、后置、最终通知都没有实现方法拦截器接口:MethodInterceptor,所以要在其类上先进行一层适配实现,使其方法能够被增强,AspectJMethodBeforeAdvice —> MethodBeforeAdviceInterceptor、AspectJAfterReturningAdvice —> AfterReturningAdviceInterceptor

3、原本所有 Advice 都可以实现 MethodInterceptor 接口,但是为了提高可扩展性,提供了适配器的模式,那么在进行 MethodInterceptor 组装的过程中,需要多加一些额外的判断逻辑,不能被添加两次,所以才需要把未实现 MethodInterceptor 接口的某些 Advice 直接通过适配器方式来实现,而不再需要通过原来的方式

4、组装拦截器链条是通过拓扑排序方式来进行组合的,会先挑选没有前驱节点的元素先进行执行;在 CGLIB 是通过 CglibMethodInvocation 来启动 Advice 通知,它又是 ReflectiveMethodInvocation 类型的子类,JDK 是通过它来启动 Advice 通知的

下图是通过 Debug 断点获取到的所有拦截器

观察 AOP 注解方式读取准备工作>代码部分,将所有的 AOP 注解放开,修改测试基类如下:

public class TestAnnotationAop {
    public static void main(String[] args) throws NoSuchMethodException {
      saveGeneratedCGlibProxyFiles(System.getProperty("user.dir")+"/proxy");
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(SpringConfiguration.class);
        ac.refresh();
        MyCalculator bean = ac.getBean(MyCalculator.class);
        System.out.println(bean.add(1, 1));
    }
    public static void saveGeneratedCGlibProxyFiles(String dir) throws Exception {
        Field field = System.class.getDeclaredField("props");
        field.setAccessible(true);
        Properties props = (Properties) field.get(null);
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, dir);//dir为保存文件路径
        props.put("net.sf.cglib.core.DebuggingClassWriter.traceEnabled", "true");
    }
}

通过 Debug 断点获取到的拦截器链以及创建方法执行的旅程 > 重要的入口:wrapIfNecessary,如下图:

JDK 生成动态代理对象过程

JDK 生成动态代理对象过程,如下:

  1. 获取目标类上的接口并且判断是否需要添加 SpringProxy、Advised、DecoratingProxy 接口
  2. 判断获取到的接口是否定义了 equals、hashCode 方法,一般是没有定义的,若有定义 equals 方法,标识 equalsDefined 为 true;若有定义 hashCode 方法,标识 hashCodeDefined 为 true
  3. 设置好接口和属性以后,创建我们的代理对象,Proxy.newProxyInstance(classLoader, proxiedInterfaces, this),基本的三件套

第一个参数是类加载器

第二个参数是在第一步获取到的所有接口

第三个参数是当前的 JdkDynamicAopProxy 类,它实现了 InvocationHandler 接口,后续在它的 invoke 方法调用处理.

动态代理执行流程

CGLIB 执行流程

如下图,AOP 拦截器对代码进行增强处理的详细执行流程图

上图中虽然能看出 Advice 执行的具体顺序,但是根据你实际的配置情况,顺序可能有所不同,所以要根据你的业务代码来判断具体要先执行那一个

当生成代理对象之后,会进行具体的逻辑方法调用,此时,AOP 若是 5 个增强通知都能适配的话,一共会有 6 个 Advisor,除了五种增强通知以外,还有一个是 ExposeInvocationInterceptor,它起着一个桥梁的作用,会将当前 MethodInvocation 对象存入线程上下文对象,它们在执行时是按照某个顺序来执行的,而且是由一个通知跳转到另外一个通知执行;所以此时,我们需要构建一个拦截器链条(责任链模式)只有创建好链式结构,才能顺利的向下执行。

1、首先它会通过切入点表达式,先进行类、方法匹配,获取到匹配的通知之后,会判别当前通知是否实现了 MethodInterceptor 接口,若没有实现的话,它会先作一层适配

2、前置、后置、最终通知都没有实现方法拦截器接口:MethodInterceptor,所以要在其类上先进行一层适配实现,使其方法能够被增强,AspectJMethodBeforeAdvice —> MethodBeforeAdviceInterceptor、AspectJAfterReturningAdvice —> AfterReturningAdviceInterceptor

3、原本所有 Advice 都可以实现 MethodInterceptor 接口,但是为了提高可扩展性,提供了适配器的模式,那么在进行 MethodInterceptor 组装的过程中,需要多加一些额外的判断逻辑,不能被添加两次,所以才需要把未实现 MethodInterceptor 接口的某些 Advice 直接通过适配器方式来实现,而不再需要通过原来的方式

4、组装拦截器链条是通过拓扑排序方式来进行组合的,会先挑选没有前驱节点的元素先进行执行;在 CGLIB 是通过 CglibMethodInvocation 来启动 Advice 通知,它又是 ReflectiveMethodInvocation 类型的子类,JDK 是通过它来启动 Advice 通知的

下图是通过 Debug 断点获取到的所有拦截器

观察 AOP 注解方式读取准备工作>代码部分,将所有的 AOP 注解放开,修改测试基类如下:

public class TestAnnotationAop {
    public static void main(String[] args) throws NoSuchMethodException {
      saveGeneratedCGlibProxyFiles(System.getProperty("user.dir")+"/proxy");
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(SpringConfiguration.class);
        ac.refresh();
        MyCalculator bean = ac.getBean(MyCalculator.class);
        System.out.println(bean.add(1, 1));
    }
    public static void saveGeneratedCGlibProxyFiles(String dir) throws Exception {
        Field field = System.class.getDeclaredField("props");
        field.setAccessible(true);
        Properties props = (Properties) field.get(null);
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, dir);//dir为保存文件路径
        props.put("net.sf.cglib.core.DebuggingClassWriter.traceEnabled", "true");
    }
}

MyCalculator#add 方法进行调用时,找到保存到本地生成的代理对象上,会看到如下的调用:

// CGLIB$CALLBACK_0:就是拦截器链的第一个拦截器:DynamicAdvisedInterceptor
public final Integer add(Integer var1, Integer var2) throws NoSuchMethodException {        
  MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;        
  if (var10000 == null) {            
    CGLIB$BIND_CALLBACKS(this);            
    var10000 = this.CGLIB$CALLBACK_0;        
  }        
  return var10000 != null ? (Integer)var10000.intercept(this, CGLIB$add$4$Method, new Object[]{var1, var2}, CGLIB$add$4$Proxy) : super.add(var1, var2);    
}

CGLIB$CALLBACK_0 指的就是 DynamicAdvisedInterceptor 拦截器,会调用 intercept 方法

  1. 执行顺序大致如上,在排序 Advisor 时,会出现前置通知执行在前或环绕通知执行在前的情况,这种顺序是不固定的

在编写 Around 最终增强通知的方法逻辑时,需要通过 ProceedingJoinPoint#proceed 方法的调用来执行下一条链路,若没有编写此方法实现的话,这时候程序执行就不完整了,链路到此处就没有办法去获取下一个拦截器去进行处理.

  1. 在源码处理中,基本上所有的增强通知都是调用的 ReflectiveMethodInvocation#proceedJoinpoint#proceed 方法,因为它们需要先去判定是否还有下一个拦截器,若还有的话就继续调用,没有的话就执行当前方法中的剩余逻辑.
// 来自于 ReflectiveMethodInvocation 源码,其子类 CglibMethodInvocation 也是调用的这部分实现
// super.process():当前调用的是 CGLIB 动态代理
// mi.process():当前调用的是 JDK 动态代理
public Object proceed() throws Throwable {
  // 从索引为-1的拦截器开始调用,并按序递增,如果拦截器链中的拦截器迭代调用完毕,开始调用 target 函数,这个函数是通过反射机制完成的
  // 具体实现在 AopUtils.invokeJoinPointUsingReflection 方法中
  if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
    return invokeJoinpoint();
  }
  // 获取下一个要执行的拦截器,沿着定义好的 interceptorOrInterceptionAdvice 链进行处理
  Object interceptorOrInterceptionAdvice =
    this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
  if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
    // 这里对拦截器进行动态匹配的判断,这里是对pointcut触发进行匹配的地方,如果和定义的pointcut匹配,那么这个advice将会得到执行
    InterceptorAndDynamicMethodMatcher dm =
      (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
    Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
    if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
      return dm.interceptor.invoke(this);
    }
    else {
      // 如果不匹配,那么 proceed 会被递归调用,直到所有的拦截器都被运行
      return proceed();
    }
  }
  else {
    // 普通拦截器,直接调用拦截器,将 this 作为参数传递以保证当前实例中调用链的执行
    return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
  }
}

总结

CGLIB 和 JDK 动态代理实现,区别是 CGLIB 是采用继承类的方式实现,如果方法中为 final 或 static 将不会被代理增强,生成的代理类会比较多;而 JDK 是采用实现接口的方式;如果它包含的接口中有存在 equals 或 hashCode 方法时将不会被代理增强,生成的代理类只有一个;它们的底层实现都是通过 ASM 字节码框架操作的.

两者在处理整个流程时,都是先将所有的 advisor 进行匹配,然后再将需要进行增强的 advisor 拦截器找到并组装起来,等待调用实际逻辑方法时会调用生成处理类的地方,按照链条一个个执行.

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

目录
相关文章
|
3月前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
431 0
|
2月前
|
XML Java 数据格式
《深入理解Spring》:AOP面向切面编程深度解析
Spring AOP通过代理模式实现面向切面编程,将日志、事务等横切关注点与业务逻辑分离。支持注解、XML和编程式配置,提供五种通知类型及丰富切点表达式,助力构建高内聚、低耦合的可维护系统。
|
7月前
|
监控 安全 Java
Spring AOP实现原理
本内容主要介绍了Spring AOP的核心概念、实现机制及代理生成流程。涵盖切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)等关键概念,解析了JDK动态代理与CGLIB代理的原理及对比,并深入探讨了通知执行链路和责任链模式的应用。同时,详细分析了AspectJ注解驱动的AOP解析过程,包括切面识别、切点表达式匹配及通知适配为Advice的机制,帮助理解Spring AOP的工作原理与实现细节。
1114 13
|
4月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
4月前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
|
10月前
|
XML Java 测试技术
Spring AOP—通知类型 和 切入点表达式 万字详解(通俗易懂)
Spring 第五节 AOP——切入点表达式 万字详解!
728 25
|
10月前
|
XML 安全 Java
Spring AOP—深入动态代理 万字详解(通俗易懂)
Spring 第四节 AOP——动态代理 万字详解!
448 24
|
10月前
|
IDE Java 应用服务中间件
spring boot 启动流程
Spring Boot 启动流程简介: 在使用 Spring Boot 之前,启动 Java Web 应用需要配置 Web 容器(如 Tomcat),并将应用打包放入容器目录。而使用 Spring Boot,只需运行 main() 方法即可启动 Web 应用。Spring Boot 的核心启动方法是 SpringApplication.run(),它负责初始化和启动应用上下文。 主要步骤包括: 1. **应用启动计时**:使用 StopWatch 记录启动时间。 2. **打印 Banner**:显示 Spring Boot 的 LOGO。 3. **创建上下文实例**:通过反射创建
548 5
|
9月前
|
Java API 微服务
微服务——SpringBoot使用归纳——Spring Boot中的切面AOP处理——Spring Boot 中的 AOP 处理
本文详细讲解了Spring Boot中的AOP(面向切面编程)处理方法。首先介绍如何引入AOP依赖,通过添加`spring-boot-starter-aop`实现。接着阐述了如何定义和实现AOP切面,包括常用注解如`@Aspect`、`@Pointcut`、`@Before`、`@After`、`@AfterReturning`和`@AfterThrowing`的使用场景与示例代码。通过这些注解,可以分别在方法执行前、后、返回时或抛出异常时插入自定义逻辑,从而实现功能增强或日志记录等操作。最后总结了AOP在实际项目中的重要作用,并提供了课程源码下载链接供进一步学习。
1087 0