Spring 深度内核-核心容器与扩展机制-AOP 进阶:AspectJ 集成、LTW 织入与工程实践

概述

在[上篇《AOP 原理剖析:代理创建、拦截链与通知顺序》]中,我们深度剖析了 Spring AOP 的三大核心机制:代理创建 (JDK 动态代理与 CGLIB)、拦截器链构建 以及 ReflectiveMethodInvocation 的递归执行 ,并明确了五种通知类型的精确执行顺序。上篇的终点,恰恰是本文的起点。上篇结束时,我们留下了一系列悬而未决的难题:为何 this.methodB() 永远无法触发通知?@Aspect 注解的类是如何被拆解成一个个 Advisor 的?execution 表达式背后到底发生了什么?AspectJ 仅仅是个注解库吗?

本文将正面迎击这些问题,揭开 Spring AOP 的能力天花板------自调用失效的根源、切点表达式的性能陷阱,并引入 AspectJ 这一强大的盟友,特别是通过加载期织入(Load-Time Weaving, LTW),展示如何从根本上突破代理模式的设计限制。

核心要点:

  • @Aspect 解析 :一个被 @Aspect 标注的切面类,在容器启动时会被 BeanFactoryAspectJAdvisorsBuilder 扫描,并由 ReflectiveAspectJAdvisorFactory 将其中的每个通知方法拆解为独立的 Advisor,形成一条完整的通知链。
  • 切点表达式executionwithin@annotationargsbean 等表达式绝非简单的字符串匹配。它们会被编译为 PointcutExpression 对象,并通过 MethodMatcherisRuntime() 方法区分静态匹配动态匹配,这是性能优劣的分水岭。
  • 三种织入方式:AspectJ 提供了编译期织入(CTW)、后编译期织入(Binary Weaving)和加载期织入(LTW)三种方式,它们在织入时机、性能和侵入性上存在根本性差异。
  • LTW 原理 :通过 Java 5+ 的 java.lang.instrument API 和 ClassFileTransformer,在 JVM 加载类文件时直接修改其字节码,从而将"代理对象"的概念从 JVM 层面消除,让增强后的类本身成为目标对象。
  • 自调用根治AopContext.currentProxy() 方案是一种"打补丁"式的妥协方案,而 LTW 通过修改 this 的真实字节码,实现了对内部调用的彻夜拦截,是该问题的终极方案。
  • 引介增强 :通过 @DeclareParents,可以动态地让目标类的所有实例"实现"一个新接口,这背后是 DelegatingIntroductionInterceptor 的巧妙设计。

文章组织架构图:

graph TD subgraph S1 ["1. 上篇回顾"] 1A["代理创建"] 1B["拦截器链"] 1C["通知顺序"] end subgraph S2 ["2. Aspect 解析"] 2A["扫描Aspect Bean"] 2B["提取通知方法"] 2C["封装为Advisor"] end subgraph S3 ["3. 切点表达式"] 3A["语法体系"] 3B["静态匹配"] 3C["动态匹配"] end subgraph S4 ["4. AspectJ 织入方式"] 4A["编译期织入"] 4B["后编译期织入"] 4C["加载期织入"] end subgraph S5 ["5. LTW 详解"] 5A["ClassFileTransformer 原理"] 5B["Spring 集成配置"] 5C["织入时机与局限"] end subgraph S6 ["6. 自调用根治"] 6A["代理模式缺陷"] 6B["expose-proxy 方案"] 6C["LTW 终极方案"] end subgraph S7 ["7. 引介增强"] 7A["DeclareParents 使用"] 7B["DelegatingIntroductionInterceptor 原理"] 7C["代理结构"] end subgraph S8 ["8. 性能与实践"] 8A["代理成本"] 8B["动态切点陷阱"] 8C["最佳实践"] end subgraph S9 ["9. 生产事故"] 9A["性能雪崩"] 9B["类加载冲突"] 9C["类型转换异常"] end subgraph S10 ["10. 面试专题"] 10A["基础对比"] 10B["原理深挖"] 10C["系统设计"] end S1 --> S2 S2 --> S3 S3 --> S4 S4 --> S5 S5 --> S6 S6 --> S7 S7 --> S8 S8 --> S9 S9 --> S10

第一层:地基与过渡(模块 1)

这一层是全文的起点,起到承上启下的作用。

  • 模块 1 -- 上篇回顾 :快速唤醒读者对上篇三大核心的记忆------代理创建 (JDK vs CGLIB)、拦截器链 (Advisor 链的构建)和通知顺序(递归执行模型)。它既划清了本文与上篇的边界,又自然引出上篇未能解决的代理式设计的遗留问题,为后续的深入剖析铺平了道路。

第二层:机制深化------从注解到匹配(模块 2 → 3)

这一层将 Spring AOP 的黑盒彻底打开,完成从"使用"到"理解"的跨越。

  • 模块 2 -- @Aspect 解析 :揭秘一个 @Aspect 类是如何被容器发现、由 BeanFactoryAspectJAdvisorsBuilder 扫描,并经由 ReflectiveAspectJAdvisorFactory 拆解为一个个 Advisor 的完整流水线。掌握此流程,是理解 Advisor 顺序和通知执行机制的前提。
  • 模块 3 -- 切点表达式 :在知道 Advisor 如何生成后,紧接着深入其最核心的组件------切点表达式。这里全面梳理 executionwithinargs@annotationbean 等语法,并聚焦 AspectJExpressionPointcut 的源码,剖析静态匹配动态匹配的根本差异。这一区分是后续性能讨论的理论基石。

第三层:能力跃迁------突破代理天花板(模块 4 → 5 → 6)

在揭示 Spring AOP 的匹配原理后,本层直面其能力边界,并引入 AspectJ 实现质的突破。

  • 模块 4 -- AspectJ 织入方式 :先跳出 Spring 的视野,全景式介绍 AspectJ 的三种织入方式:编译期织入(CTW)后编译期织入加载期织入(LTW),并对比它们的时机、性能和侵入性。这让读者建立起"织入 > 代理"的宏观认知。
  • 模块 5 -- LTW 详解 :聚焦 Spring 如何通过 LoadTimeWeaverClassFileTransformer 将 AspectJ 的 LTW 能力无缝集成。从 JVM 的 Instrumentation API 到 AspectJWeavingEnabler 的源码,完整演示三种配置方式的原理与实操。
  • 模块 6 -- 自调用根治 :针对上篇遗留的"自调用失效"顽疾,通过序列图对比代理模式LTW 模式 的调用路径,直观展示为何 this 在代理下是原始对象,而在 LTW 下就是增强后的对象。并依次展示 expose-proxy 妥协方案与 LTW 根治方案的代码示例和优劣。

第四层:实战升华------高阶特性与工程智慧(模块 7 → 8 → 9 → 10)

在完成理论与技术的武装后,本层回归工程实践,提供从高级特性到生产避坑,再到面试突击的全套武器。

  • 模块 7 -- 引介增强 :介绍一种特殊的 AOP 能力------通过 @DeclareParents 动态让目标类实现接口。结合 DelegatingIntroductionInterceptor 的源码和类图,阐明其委托机制,并提前警示代理剥离时的 ClassCastException 陷阱。
  • 模块 8 -- 性能与实践 :将所有模块累积的性能知识系统化,从代理创建成本、拦截器链开销,到动态切点这个性能杀手,最终凝结成一组可直接落地的最佳实践清单。
  • 模块 9 -- 生产事故 :通过三个血淋淋的线上案例(CPU 雪崩、类加载冲突、类型转换异常),将前面的原理知识转化为排查能力。每个案例都遵循"现象 → 思路 → 根因 → 方案"的闭环,强化读者的问题解决直觉。
  • 模块 10 -- 面试专题:作为独立模块,将全文所有知识点浓缩为 15 道高频面试题(含 1 道系统设计题)。通过"标准回答 + 多角度追问 + 加分回答"的结构,帮助读者完成知识的提取与内化,从容应对技术对话。

整张架构图 呈现出一条严谨的逻辑链:从回顾 旧知开始,逐步深化内部机制 ,然后突破能力边界 ,最终在实战场景中完成知识闭环。每一层都是下一层的铺垫,每一模块都精准回答了前一模块所引发的"为什么"和"怎么做"。


模块 1:上篇回顾与本文定位

上篇《AOP 原理剖析》中,我们完成了对 Spring AOP 核心骨架的搭建:

  1. 代理创建 :我们明确了 Spring 如何根据目标类是否实现接口,选择 JDK 动态代理或 CGLIB 来创建代理对象。这个过程发生在 Bean 生命周期的 initializeBean 之后,通过 BeanPostProcessorAbstractAutoProxyCreator)介入。
  2. 拦截器链 :我们详细剖析了 AdvisorAdviceMethodInterceptor 的关系,以及如何将一系列通知适配为 MethodInterceptor 链。
  3. 通知顺序 :我们通过 ReflectiveMethodInvocationproceed() 递归调用逻辑,清晰演示了 @Around@Before@After@AfterReturning@AfterThrowing 的执行顺序和堆栈结构。

以上知识构成了使用和调试 Spring AOP 的基础。然而,上篇内容建立在一个核心前提之上:所有的 AOP 逻辑都运行在由外部调用的代理对象上。这个前提带来了一系列上篇无法回答的问题:

  • 设计缺陷 :当 Service 内部方法 A 调用方法 B 时,这个调用是通过 this.methodB() 发生的,this 指向的是原始对象,而不是外部的代理对象。因此,methodB 上的所有切面逻辑都会失效。这个"自调用"问题,根植于 Spring AOP 的代理式设计。
  • 机制黑盒@Around@Before 等注解是如何被解析的?一个 @Aspect 类什么时候变成一个 Advisor 列表?这个过程对日常开发而言是透明的,但对于排错和理解 Advisor 的顺序至关重要。
  • 性能底层execution(* com.example.service.*.*(..)) 这个表达式,Spring 是每次都去解析字符串吗?不同类型的表达式性能有何差异?为何说 args 表达式是性能杀手?
  • 生态定位:AspectJ 是一个独立的 AOP 框架,Spring AOP 和它到底是什么关系?是包含关系,还是借用关系?

本文将逐一解答这些问题。我们先从 @Aspect 类的解析开始。


模块 2:@Aspect 类的解析:从切面到 Advisor

在 Spring 容器启动时,所有被 @Aspect 标注的类会被当做一个特殊的 Bean。AnnotationAwareAspectJAutoProxyCreator 这个 BeanPostProcessor 会负责处理所有需要增强的 Bean,而它的核心助手之一,就是 BeanFactoryAspectJAdvisorsBuilder

2.1 核心源码解读:BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors()

这个方法会在容器中扫描所有 Bean,找到 @Aspect 类,并将其解析为 Advisor 列表。

java 复制代码
// 代码来源: org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder
public List<Advisor> buildAspectJAdvisors() {
    List<String> aspectNames = this.aspectBeanNames;

    // 首次调用时,aspectBeanNames 为 null,触发初始化
    if (aspectNames == null) {
        synchronized (this) {
            aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
                List<Advisor> advisors = new ArrayList<>();
                aspectNames = new ArrayList<>();
                // 1. 从容器中获取所有 Bean 的名称
                String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                        this.beanFactory, Object.class, true, false);
                for (String beanName : beanNames) {
                    // ... 省略 bean 类型和前缀过滤逻辑 ...
                    if (this.advisorFactory.isAspect(beanType)) {
                        aspectNames.add(beanName);
                        // 2. 获取到了 @Aspect 类的一个实例(可能是 FactoryBean 获取的真实 Bean)
                        Object aspectInstance = this.beanFactory.getBean(beanName);
                        // 3. 核心:将 @Aspect 实例解析为一系列 Advisor
                        List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(aspectInstance);
                        // ... 省略缓存逻辑 ...
                        advisors.addAll(classAdvisors);
                    }
                }
                this.aspectBeanNames = aspectNames;
                this.advisorsCache = advisors;
                return advisors;
            }
        }
    }
    // ... 省略取缓存逻辑 ...
}

源码解读:

  • 双重检查锁定 :通过 aspectBeanNames == null 和内部的 synchronized 块,保证了 Advisor 的解析过程仅在容器启动时执行一次,之后直接从缓存获取,这是典型的单例懒加载模式。
  • 全局扫描BeanFactoryUtils.beanNamesForTypeIncludingAncestors 会从当前 BeanFactory 及其所有祖先容器中获取所有 Bean 名称,确保不会遗漏任何切面。
  • 代理模式工厂this.advisorFactory.isAspect(beanType) 使用 AbstractAspectJAdvisorFactory 来判断一个类是否为 @Aspect 注解的类。
  • 委托解析this.advisorFactory.getAdvisors(aspectInstance) 是逻辑核心,该方法将具体的通知方法提取逻辑委托给了 ReflectiveAspectJAdvisorFactory

2.2 ReflectiveAspectJAdvisorFactory 的工作流程

ReflectiveAspectJAdvisorFactory.getAdvisors() 是真正将 @Aspect 类拆解为 Advisor 的地方。

java 复制代码
// 代码来源: org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
    // ... 参数校验 ...
    Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
    String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
    validate(aspectClass);

    // 装饰器模式,确保切面实例的单例性
    MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
            new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

    List<Advisor> advisors = new ArrayList<>();
    // 遍历切面类的所有方法,排除 @Pointcut 方法
    for (Method method : getAdvisorMethods(aspectClass)) {
        // 1. 核心:将单个通知方法(如 @Before)转换为 Advisor
        Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
        if (advisor != null) {
            advisors.add(advisor);
        }
    }

    // ... 省略处理引介增强的 @DeclareParents 逻辑 ...

    return advisors;
}

private Advisor getAdvisor(Method candidateAdviceMethod, ...) {
    // 2. 提取切点表达式
    AspectJExpressionPointcut expressionPointcut = getPointcut(
            candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
    if (expressionPointcut == null) {
        return null;
    }
    // 3. 使用切点和通知方法创建一个 Advisor 实现
    return new InstantiationModelAwarePointcutAdvisorImpl(
            expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

源码解读:

  • 方法筛选getAdvisorMethods(aspectClass) 会获取类中除了 @Pointcut 标注之外的所有方法。Spring 对通知方法的排序规则(AfterReturning 在最前)也是在此阶段完成的。
  • 一对一映射 :每个通知方法(@Before, @After, @Around 等)都会被封装成一个独立的 InstantiationModelAwarePointcutAdvisorImpl 实例。一个包含 3 个通知方法的 @Aspect 类,最终会生成 3 个 Advisor
  • 表达式解析getPointcut 方法会解析通知注解上的 value 属性(例如 @Before("execution(* com..*.*(..))")),并将其封装为 AspectJExpressionPointcut 对象。
  • 封装为 AdvisorInstantiationModelAwarePointcutAdvisorImplAdvisor 的具体实现。它组合了 Pointcut(切点)和 Advice(通知),是构成拦截器链的基本单元。

@Aspect 类解析为 Advisor 列表的流程图:

sequenceDiagram participant Container participant Builder as BeanFactoryAspectJAdvisorsBuilder participant Factory as ReflectiveAspectJAdvisorFactory participant Advisor as InstantiationModelAwarePointcutAdvisorImpl Container->>Builder: 1. 调用buildAspectJAdvisors() Builder->>Builder: 2. 遍历所有Bean,找出@Aspect类 loop 对每个@Aspect类 Builder->>Factory: 3. 调用getAdvisors(aspectInstance) Factory->>Factory: 4. 遍历切面类的所有通知方法 loop 对每个通知方法 Factory->>Factory: 5. 提取切点表达式并封装为AspectJExpressionPointcut Factory->>Advisor: 6. new InstantiationModelAwarePointcutAdvisorImpl(pointcut, method) Advisor-->>Factory: 返回Advisor对象 end Factory-->>Builder: 返回List end Builder-->>Container: 返回所有解析好的Advisor列表

图表分层说明:

  • 主旨概括 :该图展示了 Spring 容器在启动阶段,如何将 @Aspect 类解析为一组 Advisor 的全过程。
  • 逐层分解
    1. 触发解析BeanFactoryAspectJAdvisorsBuilder 被调用,启动整个解析流程。
    2. 全局扫描 :它遍历 BeanFactory 中的所有 Bean 定义,筛选出所有标注了 @Aspect 注解的类。
    3. 委托解析 :对于每个 @Aspect 类,它委托 ReflectiveAspectJAdvisorFactory 进行详细解析。
    4. 方法提取 :工厂遍历切面类中所有带有 @Before@Around 等通知注解的方法。
    5. 对象封装 :为每个通知方法创建一个 InstantiationModelAwarePointcutAdvisorImpl 实例,该实例组合了切点(Pointcut)通知(Advice)
  • 设计原理 :这是典型的单一职责装饰器模式 的运用。Builder 负责扫描与协调,Factory 负责具体的解析逻辑,而 Impl 类则是最终产物的封装。这种设计使得每个部分职责清晰,易于扩展和测试。
  • 工程联系与结论 :在日常开发中,我们通过 List<Advisor> 注入或在 Debug 模式下查看 BeanFactory,可以直观地验证这一解析结果。理解此流程至关重要 ,因为它决定了通知的执行顺序:解析出的 Advisor 列表的顺序,直接影响了拦截器链的构建顺序,进而决定了同一方法上多个通知的执行先后。如果发现通知执行顺序与预期不符,需要返回去检查 @Aspect 类中方法的定义顺序和类型。

内联示例 1:验证 @Aspect 解析

此示例演示了如何通过注入 Advisor 列表,直观地看到 @Aspect 类的解析结果。

pom.xml 依赖(仅展示核心):

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.24</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.3.24</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.7</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
    </dependency>
</dependencies>

配置类 AppConfig.java

java 复制代码
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}

目标类和包含三种通知的切面 LoggingAspect.java

java 复制代码
// 目标类
package com.example.service;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public String getUser(String name) {
        return "User: " + name;
    }
}

// 切面类
package com.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.UserService.getUser(..))")
    public void beforeAdvice() {
        System.out.println("[Before] 执行权限校验...");
    }

    @Around("execution(* com.example.service.UserService.getUser(..))")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[Around] 开启事务...");
        Object result = pjp.proceed();
        System.out.println("[Around] 提交事务...");
        return result;
    }

    @After("execution(* com.example.service.UserService.getUser(..))")
    public void afterAdvice() {
        System.out.println("[After] 释放资源...");
    }
}

测试类 AspectParsingTest.java

java 复制代码
import com.example.AppConfig;
import org.junit.Test;
import org.springframework.aop.Advisor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;

public class AspectParsingTest {
    @Test
    public void testAspectParsingToAdvisors() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        
        // 通过注入 Advisors 来验证解析结果
        // 注意:这里需要从容器中获取,在实际项目中可 @Autowired
        // 为演示方便,此处从容器名获取
        List<Advisor> advisors = context.getBeansOfType(Advisor.class).values().stream().toList();
        
        System.out.println("=== 解析出的 Advisor 列表 ===");
        for (Advisor advisor : advisors) {
            System.out.println("Advisor: " + advisor.getClass().getSimpleName() 
                + " -> " + advisor.getAdvice().getClass().getSimpleName());
        }
        
        // 输出类似于:
        // Advisor: InstantiationModelAwarePointcutAdvisorImpl -> AroundAdvice
        // Advisor: InstantiationModelAwarePointcutAdvisorImpl -> BeforeAdvice
        // Advisor: InstantiationModelAwarePointcutAdvisorImpl -> AfterAdvice
        // 验证:每个通知方法确实生成了一个对应的 Advisor 实现
        
        context.close();
    }
}

代码注释与验证 : 此测试启动 Spring 容器后,从 BeanFactory 中获取所有 Advisor 类型的 Bean。我们可以看到,LoggingAspect 中的三个方法(beforeAdvice, aroundAdvice, afterAdvice)分别被封装成独立的 InstantiationModelAwarePointcutAdvisorImpl,其内部的 Advice 类型也各不相同,这与我们上面的源码分析完全一致。


模块 3:切点表达式的语法体系与匹配原理

切点表达式是 AOP 的"寻址魔法",决定了通知逻辑将被织入到哪些目标的哪些方法上。Spring AOP 采用了 AspectJ 的切点表达式语法,但这套语法背后有复杂的匹配和优化逻辑。

3.1 语法全景

| 表达式类型 | 格式 | 示例 | 说明与匹配维度 |
|:----------------|:---------------------------------------------|:---------------------------------------------------------------------------|:-----------------------------------------------------------------|---------------------------------------------------------|---------------------------|---|---------------|
| execution | execution(修饰符? 返回类型 声明类型? 方法名(参数列表) 异常类型?) | execution(public String com.example.service.UserService.getUser(String)) | 方法签名维度 :最强大、最常用的表达式,可精确到返回类型、方法名、参数类型。*.. 是通配符。 |
| within | within(类型表达式) | within(com.example.service.*) | 类维度 :匹配指定类或包下所有类的所有方法。粒度比 execution 粗,只能到类级别,无法区分方法。 |
| this | this(类型) | this(com.example.service.UserService) | 代理对象维度:匹配代理对象是指定类型的方法。在 JDK 代理下,代理对象实现了接口;在 CGLIB 下是子类。 |
| target | target(类型) | target(com.example.dao.UserDao) | 目标对象维度:匹配目标对象(被代理对象)是指定类型的方法。 |
| args | args(参数类型, ...) | args(java.lang.String, ..) | 运行时参数维度 :匹配方法参数在运行时是指定类型实例的方法。注意此表达式会强制动态匹配。 |
| @annotation | @annotation(注解类型) | @annotation(com.example.Log) | 方法注解维度:匹配方法上带有指定注解的方法。 |
| @within | @within(注解类型) | @within(com.example.Auditable) | 类注解维度:匹配类上带有指定注解的类的所有方法。 |
| @target | @target(注解类型) | @target(org.springframework.stereotype.Service) | 目标对象注解维度:匹配目标对象的类上带有指定注解的方法。 |
| @args | @args(注解类型) | @args(com.example.NotNull) | 运行时参数注解维度:匹配方法的运行时入参中,参数类上带有指定注解的方法。 |
| bean | bean(Bean名称) | bean(userService) | Spring Bean 维度 :Spring AOP 的扩展表达式,按 Bean 的名称进行匹配,支持 * 通配符。 |
| 组合运算 | 表达式1 && 表达式2、`\ | \ | !表达式` | execution(* get*(..)) && @annotation(com.example.Log) | 可将多个切点表达式进行逻辑与(&&)、或(` | | )、非(!`)组合。 |

3.2 源码核心:AspectJExpressionPointcut

所有的切点表达式最终都会被封装为一个 AspectJExpressionPointcut 对象。它负责编译表达式并完成匹配判断。

java 复制代码
// 代码来源: org.springframework.aop.aspectj.AspectJExpressionPointcut
public class AspectJExpressionPointcut implements Pointcut, BeanFactoryAware {
    // ... 省略其他代码 ...

    // 缓存编译好的 AspectJ 表达式对象
    @Nullable
    private PointcutExpression pointcutExpression;

    // 核心:获取 MethodMatcher
    public MethodMatcher getMethodMatcher() {
        obtainPointcutExpression(); // 确保表达式已编译
        return this;
    }

    // 实现了 MethodMatcher 接口,本身就是一个 MethodMatcher
    public boolean matches(Method method, @Nullable Class<?> targetClass) {
        // 1. 编译表达式
        PointcutExpression pointcutExpression = obtainPointcutExpression();
        // 2. 创建一个 AspectJ 的 ShadowMatch,将 Java Method 映射为 AspectJ 的连接点
        ShadowMatch shadowMatch = getShadowMatch(method, targetClass);
        // 3. 总是匹配(alwaysMatches)或部分匹配
        if (shadowMatch.alwaysMatches()) {
            return true;
        } else if (shadowMatch.neverMatches()) {
            return false;
        }
        // 4. 对非绝对的匹配,进行更复杂的判断逻辑...
        return matches(shadowMatch);
    }

    // 判断是否需要运行时匹配(动态匹配)
    public boolean isRuntime() {
        return obtainPointcutExpression().mayNeedDynamicMatch();
    }

    // 动态匹配(每次方法调用时执行)
    public boolean matches(Method method, @Nullable Class<?> targetClass, Object... args) {
        // ... 获取 ShadowMatch
        ShadowMatch shadowMatch = getShadowMatch(method, targetClass);
        ShadowMatch originalShadowMatch = getShadowMatch(method, targetClass, false);
        
        // 提取运行时参数绑定
        // ... 详细绑定逻辑 ...
        
        // 最终调用 AspectJ 的 matchesJoinPoint 方法,传入运行时参数
        return pointcutExpression.matchesJoinPoint(
                thisJoinPoint, shadowMatch, exposedBindings);
    }

    // 编译表达式
    private PointcutExpression obtainPointcutExpression() {
        if (this.pointcutExpression == null) {
            // ... 获取 ClassLoader ...
            // 使用 AspectJ 的 PointcutParser 解析字符串并编译
            PointcutParser parser = PointcutParser.getPointcutParserSupportingSpecedPrimitivesAndUserDefinedPointcuts(cl);
            this.pointcutExpression = parser.parsePointcutExpression(this.expression);
        }
        return this.pointcutExpression;
    }
    // ... 省略其他辅助方法 ...
}

源码解读:

  • 编译与缓存obtainPointcutExpression() 方法负责将字符串表达式编译为 AspectJ 内部的 PointcutExpression 对象。这个过程很昂贵,因此编译后的对象会被缓存在 pointcutExpression 字段中,整个容器生命周期内只编译一次
  • 静态匹配matches(Method, Class) 方法只根据方法的签名和类信息进行匹配,不涉及运行时参数。例如,execution(* UserService.getUser(..)) 在启动时就能判断是否匹配。
  • 动态匹配触发isRuntime() 是区分静态/动态匹配的开关。它委托给 pointcutExpression.mayNeedDynamicMatch()。对于 @annotation(full.annotation)args(String,..) 等表达式,isRuntime() 会返回 true。这意味着这些匹配必须等到方法被真实调用,参数确定后才能进行。
  • 动态匹配执行 :当 isRuntime() 返回 true 时,Spring 在每次调用方法前都会执行 matches(Method, Class, Object...) 方法,传入真实的参数,从而进行最终判断。这就是动态切点性能开销大的根源。

AspectJExpressionPointcut 内部结构与匹配流程图:

graph TD subgraph AspectJExpressionPointcut A["字符串表达式"] --> B("PointcutParser") B --> C{"编译解析"} C -->|"成功"| D["PointcutExpression"] C -->|"失败"| E["抛出IllegalArgumentException"] D --> F{"获取 MethodMatcher"} F -->|"返回自身"| G["AspectJExpressionPointcut"] G --> H{"调用 isRuntime方法"} H -->|"返回 true"| I["动态匹配路径"] H -->|"返回 false"| J["静态匹配路径"] J --> J1["matches Method Class"] J1 --> J2["基于Java反射的方法和类信息判断"] I --> I1["matches Method Class Object..."] I1 --> I2["传入运行时参数列表"] end

图表分层说明:

  • 主旨概括 :该图详细描述了 AspectJExpressionPointcut 内部如何处理一个字符串表达式,并最终决定采用静态还是动态匹配的流程。
  • 逐层分解
    1. 编译 :字符串表达式首先经过 PointcutParser 的解析,生成一个已编译PointcutExpression 对象。
    2. 匹配判断AspectJExpressionPointcut 自身实现了 MethodMatcher 接口。外部调用者通过 isRuntime() 方法来决定走哪条匹配路径。
    3. 静态路径 :如果 isRuntime()false,则在容器启动时调用 matches(Method, Class) 一次性完成判断。
    4. 动态路径 :如果 isRuntime()true,则在每次方法调用时,传入真实的运行时参数(Object... args)进行二次判断。
  • 设计原理 :这是典型的两阶段匹配 策略。第一阶段(静态)基于不可变的类元数据,快速过滤掉大部分不相关的方法,其优点在于性能高。第二阶段(动态)基于可变的入参,提供了更强的灵活性,但代价是运行时开销。这种设计实现了性能和灵活性的权衡。
  • 工程联系与结论 :此流程是 AOP 性能优化的关键。理解 isRuntime() 何时返回 true(如使用 args@args、带通配符的 @annotation 等),能帮助我们在编写切点表达式时做出有意识的取舍。排查慢接口时,如果一个动态切点匹配了所有 Service 方法,其开销将是灾难性的。

内联示例 2:验证切点表达式的静态/动态匹配

此示例通过注入不同类型的切点,展示 isRuntime() 的返回值以及性能差异。

目标类和自定义注解 Log.java

java 复制代码
// 注解
package com.example.annotation;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
    String value() default "";
}

// 目标类
package com.example.service;
import com.example.annotation.Log;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    @Log("订单创建")
    public void createOrder(String itemName) {
        System.out.println("正在创建订单: " + itemName);
    }

    public void cancelOrder(String orderId, String reason) {
        System.out.println("正在取消订单: " + orderId + ", 原因: " + reason);
    }
}

切面 PointcutAnalysisAspect.java 和测试类 PointcutAnalysisTest.java

java 复制代码
// 切面类
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.stereotype.Component;
import org.springframework.aop.MethodMatcher;

@Aspect
@Component
public class PointcutAnalysisAspect {
    
    // 静态切点:execution
    @Before("execution(* com.example.service.OrderService.createOrder(..))")
    public void staticMatchAdvice(JoinPoint jp) {
        AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
        pc.setExpression("execution(* com.example.service.OrderService.createOrder(..))");
        MethodMatcher mm = pc.getMethodMatcher();
        // 这里只是示范如何获取,实际 Spring 不会为每个 Advice 创建新的 Pointcut
        System.out.println("[execution切点] isRuntime: " + mm.isRuntime()); // 输出 false
        System.out.println("[执行期] 静态匹配通知: " + jp.getSignature().getName());
    }

    // 动态切点:args 要求第一个参数为 String 且第二个参数也为 String
    // 注意:@Before("args(String,..)") 会导致所有第一个参数为String的方法都被增强,此处仅作演示
    @Before("execution(* com.example..*.*(..)) && args(orderId, reason)")
    public void dynamicMatchAdvice(JoinPoint jp, String orderId, String reason) {
        AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
        pc.setExpression("execution(* com.example..*.*(..)) && args(String, String)");
        MethodMatcher mm = pc.getMethodMatcher();
        System.out.println("[args切点] isRuntime: " + mm.isRuntime()); // 输出 true
        System.out.println("[执行期] 动态匹配通知: " + jp.getSignature().getName() + ", 参数: " + orderId + ", " + reason);
    }
}

// 测试类
import com.example.AppConfig;
import com.example.service.OrderService;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class PointcutAnalysisTest {
    @Test
    public void testPointcutMatching() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        OrderService service = context.getBean(OrderService.class);

        System.out.println("====== 调用 createOrder ======");
        service.createOrder("Laptop");
        // 观察日志:
        // 1. [execution切点] isRuntime: false (实际输出在启动时或第一次代理调用时)
        // 2. [执行期] 创建订单通知: createOrder
        // 3. [args切点] 没有被触发,因为参数不匹配(只有一个String)

        System.out.println("====== 调用 cancelOrder ======");
        service.cancelOrder("10086", "不想要了");
        // 观察日志:
        // 1. [args切点] isRuntime: true
        // 2. [执行期] 动态匹配通知: cancelOrder, 参数: 10086, 不想要了
        // 结论:args表达式确实触发了动态匹配,它会在每次方法调用时判断参数类型是否匹配。

        context.close();
    }
}

代码验证 : 运行测试,可以清晰地看到,execution 切点的 isRuntime() 返回 false,它只在代理创建时匹配一次。而 args 切点返回 true,它在每次 cancelOrder 被调用时,都会进入 matches(Method, Class, Object...) 流程,比对运行时参数,这揭示了其性能开销大于静态切点的根本原因。


模块 4:AspectJ 的三种织入方式对比

Spring AOP 只是借用了 AspectJ 的注解,但它并非一个完整的 AOP 方案。AspectJ 作为一个独立的、完全体的 AOP 框架,通过修改字节码的方式实现织入,不依赖任何运行时代理。

AspectJ 提供三种织入方式,分别作用于不同的生命周期阶段:

4.1 编译期织入(Compile-Time Weaving, CTW)

  • 过程 :使用 AspectJ 编译器(ajc)代替标准的 Java 编译器(javac)。ajc 在将 .java 源文件编译为 .class 字节码的同时,将切面逻辑直接织入到目标类的字节码中。
  • 优点:性能最优,因为所有织入工作在编译期完成,运行时无额外开销。启动速度快。
  • 缺点 :强依赖于 ajc 编译器,侵入构建过程,不灵活。

4.2 后编译期织入(Binary Weaving)

  • 过程 :对已经由 javac 编译好的 .class 文件或 .jar 包进行织入。这通常发生在打包(如使用 Maven 或 Gradle 插件)或部署阶段。
  • 优点:可以织入已有的第三方库,不强制要求有源码,也比 LTW 更早地完成织入。
  • 缺点:同样依赖于特定构建插件,对构建过程有侵入性。

4.3 加载期织入(Load-Time Weaving, LTW)

  • 过程 :在 JVM 使用类加载器(ClassLoader)加载 .class 文件时,动态地对字节码进行修改,将切面织入。这依赖于 Java 5+ 的 java.lang.instrument API。
  • 优点延迟绑定,灵活性最高,可以在不修改构建过程和源代码的情况下应用切面。非常适合需要按环境启用/禁用 AOP 的场景。
  • 缺点 :增加了 JVM 启动和首次类加载的时间。需要配置 -javaagent JVM 启动参数或使用特定的类加载器。

三种织入方式对比流程图:

graph LR subgraph 编译期织入 CTW direction LR A1[.java源文件] --> A2{ajc编译器}; A2 --> A3[.class字节码
含织入逻辑]; end subgraph 后编译期织入 Binary Weaving direction LR B1[.class字节码] --> B2{织入工具}; B2 --> B3[.class字节码
含织入逻辑]; end subgraph 加载期织入 LTW direction LR C1[.class字节码] --> C2{JVM类加载器}; C2 --> |ClassFileTransformer| C3[内存中的类和织入逻辑]; end A3 --> D[JVM运行时]; B3 --> D; C3 --> D;

图表分层说明:

  • 主旨概括:对比 AspectJ 三种织入方式在程序生命周期(编译、打包、加载)中的不同介入时机。
  • 逐层分解
    1. CTW :在编译期 ,使用专用的 ajc 编译器直接将切面逻辑写入字节码。
    2. Binary Weaving :在编译后、运行前 ,对已存在的 .class 文件进行后处理,将切面织入。
    3. LTW :在 JVM 加载类时 ,通过 ClassFileTransformer 拦截类的加载过程,动态修改字节码。
  • 设计原理 :这三种方式体现了 AOP 框架在织入时机上的权衡。织入越早,运行时性能越高但对构建过程侵入性越强;织入越晚,灵活性越高,但会带来启动开销。
  • 工程联系与结论:对于追求极致性能且严格控制构建流程的核心业务模块,CTW 是好选择。对于需要为第三方库或不希望修改构建流程的应用添加横切关注点,LTW 是最佳选择。Spring 对 LTW 的集成做得最好,这是我们在项目中引入 AspectJ 的主要方式。

对比表格:

特性 编译期织入 (CTW) 后编译期织入 (Binary) 加载期织入 (LTW)
织入时机 编译期 编译后、打包前 JVM 加载类时
织入器 ajc 编译器 AspectJ 工具(ajc 或插件) aspectjweaver.jar + JVM TI
对源码的侵入性 无(源码仍为标准 Java)
对构建过程的侵入性 (需替换编译器) (需集成 Maven/Gradle 插件)
运行时性能开销 极低,无额外开销 极低,无额外开销 中等(首次类加载时有织入开销)
灵活性 最高
Spring 集成难度 复杂,需特殊配置 较复杂 简单(开箱即用)

Spring AOP 与 AspectJ 的关系澄清: Spring AOP 借用了 AspectJ 的注解体系切点语法 来分析和管理切面,但在其默认代理模式下,它根本不使用 AspectJ 的织入引擎 。只有当我们配置了 LTW 并引入了 aspectjweaver.jar 时,Spring 才会真正调用 AspectJ 的字节码修改能力,从"代理模式"切换到"编织模式"。


模块 5:加载期织入(LTW)详解

LTW 是 Spring 与 AspectJ 深度集成的核心体现,也是解决 Spring AOP 代理缺陷的终极方案。

5.1 核心原理:ClassFileTransformerjava.lang.instrument

Java 5 引入了 java.lang.instrument 包,允许开发者在 JVM 加载类文件时进行拦截和修改。其核心接口是:

  • java.lang.instrument.ClassFileTransformer :只有一个 transform 方法,允许在类的原始字节码被 JVM 定义之前被修改。
  • java.lang.instrument.Instrumentation :提供注册 ClassFileTransformer 的方法。

AspectJ 的 LTW 织入器(在 aspectjweaver.jar 中)实现了 ClassFileTransformer。当 Spring 启用 LTW 时,它会监听容器刷新事件,并获取 JVM 的 Instrumentation 实例,将 AspectJ 的转换器注册进去。这样,任何后续加载的类都会先经过 AspectJ 的转换器,根据 aop.xml@Aspect 注解的定义进行字节码织入。

5.2 源码分析:Spring 如何集成

Spring 通过 LoadTimeWeaver 接口抽象了不同环境下的 LTW 实现。

  • 接口org.springframework.instrument.classloading.LoadTimeWeaver
  • 关键实现
    • InstrumentationLoadTimeWeaver:用于 Java SE 环境,直接使用 java.lang.instrument.Instrumentation
    • ReflectiveLoadTimeWeaver:用于 Web 容器环境(如 Tomcat),通过反射调用容器提供的 ClassLoader 上的方法。
  • 织入启功器org.springframework.context.weaving.AspectJWeavingEnabler。这是一个 BeanFactoryPostProcessor,它在容器启动早期被调用,其核心逻辑是拿到 LoadTimeWeaver 实例,并调用 LoadTimeWeaver.addTransformer 方法,将 AspectJ 的 ClassPreProcessorAgentAdapter 注册进去。
java 复制代码
// 代码来源: org.springframework.context.weaving.AspectJWeavingEnabler 精简版
public class AspectJWeavingEnabler extends ContextLoadTimeWeaver implements BeanFactoryPostProcessor, ... {

    public static final String ASPECTJ_AOP_XML_RESOURCE = "META-INF/aop.xml";

    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 1. 启用织入,注册 ClassFileTransformer
        enableAspectJWeaving(this.loadTimeWeaver, this.beanClassLoader);
    }

    public static void enableAspectJWeaving(@Nullable LoadTimeWeaver weaverToUse, @Nullable ClassLoader classLoader) {
        if (weaverToUse == null) {
            // 如果没有 LoadTimeWeaver,尝试根据虚拟机参数自行注册
            // 但通常会失败,除非以 -javaagent 启动
            weaverToUse = new DefaultContextLoadTimeWeaver();
        }
        weaverToUse.addTransformer(new ClassPreProcessorAgentAdapter());
        // ... 省略一些细节 ...
    }
    // ...
}

源码解读: AspectJWeavingEnabler 的核心工作就是一句话:weaverToUse.addTransformer(new ClassPreProcessorAgentAdapter())ClassPreProcessorAgentAdapter 是 AspectJ 提供的适配器,它实现了 ClassFileTransformer,内部持有真正的织入器(如 BcelWeaver 或 ASM 实现)。这一步操作,完成了从 Spring 世界到 AspectJ 世界的桥梁搭建。

5.3 Spring 集成 LTW 的三种配置方式

  • 方式一:@EnableLoadTimeWeaving 注解 这是最 Spring 风格的方式。通常在 @Configuration 类上使用,并配合 aspectjWeaving 属性。

    java 复制代码
    @Configuration
    @EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED)
    public class AppConfig { ... }

    这种方式需要 spring-instrument.jar 作为 Java Agent 使用,因为它底层依赖 InstrumentationLoadTimeWeaver

  • 方式二:META-INF/aop.xml 配置文件 这是 AspectJ 原生的配置方式,与 Spring 注解无关。创建一个 META-INF/aop.xml 文件,精确定义哪些切面织入到哪些包。

    xml 复制代码
    <aspectj>
        <aspects>
            <aspect name="com.example.aspect.LtwAspect"/>
        </aspects>
        <weaver options="-nowarn">
            <include within="com.example.service..*"/>
        </weaver>
    </aspectj>

    此方式必须在 JVM 层面启用 LTW,通常通过 -javaagent:path/to/aspectjweaver.jar 启动参数,或者配合 @EnableLoadTimeWeaving

  • 方式三:JVM 启动参数方式 这是最独立、最"硬核"的方式。在 JVM 启动时,直接使用 -javaagent 参数加载 AspectJ 织入器。 -javaagent:/path/to/aspectjweaver.jar -Daj.weaving.verbose=true 这种方式不依赖任何 Spring 配置,但可以完美地与 Spring 应用共存。

LTW 工作原理序列图:

sequenceDiagram participant App as 应用程序 participant JVM participant LL as ClassLoader participant TW as LoadTimeWeaver(Spring) participant AJT as AspectJ ClassFileTransformer participant Weaver as AspectJ Weaver App->>JVM: 1. 启动应用 (java -javaagent:spring-instrument.jar ...) JVM->>JVM: 2. 加载并初始化 agent App->>LL: 3. 请求加载类 com.example.UserService LL->>TW: 4. 询问是否需要转换 (loadTimeWeaver # transforms) TW-->>LL: 是 LL->>AJT: 5. 传递原始字节码给 transform 方法 AJT->>Weaver: 6. 委托 AspectJ 织入引擎 Weaver->>Weaver: 7. 根据 aop.xml 或注解匹配切面与类 alt 类是织入目标 Weaver->>Weaver: 8. 修改字节码,织入通知逻辑 end Weaver-->>AJT: 9. 返回修改后的字节码 AJT-->>LL: 10. 返回最终字节码 LL->>JVM: 11. 使用修改后的字节码定义类 JVM->>App: 12. 返回增强后的 UserService 实例

图表分层说明:

  • 主旨概括:该序列图描述了从应用启动到最终获取增强后的 Bean 实例,LTW 在整个类加载过程中扮演的关键角色。
  • 逐层分解
    1. 启动拦截 :应用通过 -javaagent 启动,JVM 加载了 Spring 的 Instrumentation 代理。
    2. 类加载请求 :当业务代码第一次访问 UserService 时,类加载器发起加载请求。
    3. Transformer 链介入 :类加载器在定义类之前,会调用所有注册的 ClassFileTransformertransform 方法。
    4. AspectJ 织入 :Spring 注册的 AspectJ 转换器被触发,它利用 AspectJ 的织入引擎,检查 aop.xml 或 Spring 的注解配置,判断该类是否需要增强。
    5. 字节码替换:如果需要,AspectJ 引擎会修改原始字节码并返回。JVM 最终使用的是被修改后的字节码。
  • 设计原理 :这是典型的责任链模式 在 JVM 类加载机制上的应用。ClassFileTransformer 形成了处理链,AspectJ 只是其中的一环。LoadTimeWeaver 接口在此处起到了适配器 的作用,屏蔽了不同环境下获取 Instrumentation 实例的细节。
  • 工程联系与结论 :理解此流程是排查 LTW 相关问题的关键。如果 LTW 没生效,排查思路就是沿着这条链:① JVM Agent 是否正确加载?② LoadTimeWeaver 是否匹配运行环境?③ ClassFileTransformer 是否被成功注册?④ 目标类是否在 aop.xml 或注解的匹配范围内?⑤ 织入后的类名是否会包含特殊的后缀(如 $AjcClosure)?

内联示例 3:LTW 三种配置方式演示

我们将创建三个独立的测试模块来演示这三种配置。

前提:Maven 依赖,所有模块都需要:

xml 复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-instrument</artifactId>
    <version>5.3.24</version>
    <scope>provided</scope>
</dependency>

方式一:@EnableLoadTimeWeaving 注解方式

配置类 LtwConfig1.java

java 复制代码
@Configuration
@EnableLoadTimeWeaving(aspectjWeaving = ENABLED)
@ComponentScan("com.example.service")
public class LtwConfig1 {}

启动方式:必须使用 -javaagent:/path/to/spring-instrument-5.3.24.jar JVM 参数。 测试代码 DemoTest1.java

java 复制代码
public class DemoTest1 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(LtwConfig1.class);
        UserService userService = ctx.getBean(UserService.class);
        // 验证:类名不包含 "$$EnhancerByCGLIB" 或 "$Proxy",但可能有 AspectJ 织入的内部类
        System.out.println("Bean 类名: " + userService.getClass().getName());
        // 调用方法,观察 AspectJ 切面日志是否打印,以验证织入成功
        userService.createUser("test");
        ctx.close();
    }
}

方式二:META-INF/aop.xml 方式

配置文件 src/main/resources/META-INF/aop.xml

xml 复制代码
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <aspects>
        <!-- 切面类可以是不带 @Component 的普通 POJO -->
        <aspect name="com.example.aspect.LtwXmlAspect"/>
    </aspects>
    <weaver options="-verbose -showWeaveInfo">
        <!-- 指定要织入的包 -->
        <include within="com.example.service..*"/>
    </weaver>
</aspectj>

LtwXmlAspect.java

java 复制代码
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class LtwXmlAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void xmlAspectAdvice(JoinPoint jp) {
        System.out.println("[aop.xml方式] 即将执行方法: " + jp.getSignature().getName());
    }
}

运行此示例,同样需要 -javaagent@EnableLoadTimeWeaving 来激活 LTW 环境。aop.xml 的存在只是定义了织入规则。

方式三:纯 -javaagent 方式

准备一个单独的 DemoTest3,它不包含任何 Spring LTW 配置,只有一个简单的 main 方法。

java 复制代码
public class DemoTest3 {
    public static void main(String[] args) {
        // 直接 new 出对象,而不是从 Spring 容器获取
        UserService userService = new UserService();
        System.out.println("实例类名: " + userService.getClass().getName());
        userService.createUser("aspectj-agent-test");
    }
}

启动命令java -javaagent:/path/to/aspectjweaver-1.9.7.jar -classpath ... com.example.DemoTest3

这个方式最纯粹地验证了 AspectJ 在 JVM 层面的织入能力。即便没有 Spring 容器,任何 new UserService() 创建出来的对象,其字节码都已经被 AspectJ 代理修改过了。运行后,你会发现 UserServicecreateUser 方法被 aop.xml 中定义的切面拦截了。


模块 6:自调用问题深度剖析与根治

这是 Spring AOP 中最经典、最常被问及的问题。

6.1 底层原理:this 的罪与罚

当我们在 UserService 中编写如下代码时:

java 复制代码
public void methodA() {
    System.out.println("A");
    this.methodB(); // 自调用
}

@Transactional // 或任何其它 AOP 注解
public void methodB() { ... }

this 关键字永远指向当前对象本身。在 Spring AOP 的代理模式下,methodA 的调用者是外部的 userServiceProxyuserServiceProxy 拦截调用,执行完通知逻辑后,通过 method.invoke(target, args) 将调用转发给真实的 UserService 对象的 methodA。此时,methodA 内部的 this 是真实的 UserService 对象,而不是 userServiceProxy。因此,this.methodB() 调用直接绕过了代理,所有绑定在 methodB 上的拦截器链都不会被执行。

传统解决方案与局限:

  • AopContext.currentProxy() :通过 ((UserService) AopContext.currentProxy()).methodB() 显式获取代理对象。局限 :需要配置 @EnableAspectJAutoProxy(exposeProxy = true),代码侵入性强,且要求调用方明确知道自己在使用 AOP。
  • 注入自身@Autowired private UserService self;局限 :会造成循环依赖的隐患,除非使用 @Lazy 或 setter 注入。
  • 架构重构 :将 methodB 拆分到另一个 Service 类中。局限:可能导致类爆炸,有时从业务内聚性上看并不合理。

6.2 LTW 的根治方案:字节码级的解决

LTW 从根本上解决了这个问题。当 JVM 加载 UserService.class 时,AspectJ 织入器直接修改了该类的字节码。methodAmethodB 的字节码中都内嵌了切面逻辑,而不再是需要一个外部代理来触发。

java 复制代码
// 编译期看起来一样的代码
public void methodA() {
    // LTW 后,此处字节码已被修改,可能变成类似:
    // AspectJRuntime.invokeBefore(...)
    System.out.println("A");
    this.methodB(); // this 本身已经是增强后的对象
    // AspectJRuntime.invokeAfter(...)
}

this.methodB() 被调用时,它调用的是已经被织入了切面逻辑的 methodB,因此通知会正常触发。没有代理对象,this 就是增强后的对象本身,也就不存在"绕过代理"的问题。

自调用问题排查序列图(代理模式 vs LTW 模式):

sequenceDiagram participant Caller participant Proxy as 代理对象 participant Target as 真实目标对象 Note over Caller, Target: 代理模式下的自调用 Caller->>Proxy: 1. 调用 methodA() Proxy->>Target: 2. methodA() (代理转发) Target->>Target: 3. this.methodB() (调用内部真实对象方法) Target-->>Target: 4. methodB 上的通知未触发! Target-->>Proxy: 5. 返回 Proxy-->>Caller: 6. 返回 Note over Caller, Target: LTW模式下的自调用 Caller->>Target: 1. 调用增强后的methodA() Target->>Target: 2. 执行 methodA 内部已织入的逻辑 Target->>Target: 3. this.methodB() Target->>Target: 4. 执行 methodB 内部已织入的逻辑 (通知触发!) Target-->>Caller: 5. 返回

图表分层说明:

  • 主旨概括:通过对比代理模式和 LTW 模式下的方法调用路径,直观地揭示了自调用问题为何在代理模式下发生,又为何能在 LTW 模式下被解决。
  • 逐层分解
    • 代理模式 :调用从外部的代理对象传入,但 methodA 内部的 this.methodB() 跳过了代理,直接作用于真实目标对象 ,导致 methodB 的切面失效。
    • LTW 模式 :不再有代理层。调用直接作用于被增强过的真实目标对象this.methodB() 调用的是已织入切面逻辑的本地方法。
  • 设计原理 :该图深刻体现了代理模式与编织模式的本质区别。代理模式是间接引用 (Indirection),所有对目标对象的访问都必须通过代理中转。编织模式是直接修改(Modification),从根本上改变了目标对象的行为。
  • 工程联系与结论 :排查自调用问题时,我们首先应通过打印 AopUtils.isAopProxy(bean)bean.getClass().getName() 来确认是否处于代理模式。如果确定是代理模式且遇到自调用问题,除了改造代码,启用 LTW 是唯一的、从根源上解决问题的方案。

内联示例 4:自调用问题及解决方案对比

目标类 SelfInvocationService.java

java 复制代码
package com.example.service;
import org.springframework.stereotype.Service;

@Service
public class SelfInvocationService {
    public void methodA() {
        System.out.println("进入 methodA");
        // 自调用
        this.methodB();
    }

    // 我们期望 methodB 被拦截
    public void methodB() {
        System.out.println("执行 methodB 业务逻辑");
    }
}

切面 SelfInvocationAspect.java

java 复制代码
package com.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SelfInvocationAspect {
    @Around("execution(* com.example.service.SelfInvocationService.methodB(..))")
    public Object aroundMethodB(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[Around methodB] 方法拦截 BEFORE!");
        Object result = pjp.proceed();
        System.out.println("[Around methodB] 方法拦截 AFTER!");
        return result;
    }
}

测试代码 SelfInvocationTest.java 将对比三种场景:

  1. 默认代理模式 :运行后发现,调用 service.methodA() 时,[Around methodB] 的日志不会被打印。
  2. AopContext.currentProxy() 模式 :修改 SelfInvocationService,将 this.methodB() 替换为 ((SelfInvocationService) AopContext.currentProxy()).methodB(),并添加 @EnableAspectJAutoProxy(exposeProxy = true)。再次运行,日志成功打印。
  3. LTW 模式 :在启动参数中加入 -javaagent,并配置 aop.xml@EnableLoadTimeWeaving。恢复原始的 this.methodB() 调用。运行测试,你会发现即使没有 AopContext,[Around methodB] 的日志也能成功打印。
java 复制代码
// SelfInvocationService 的 LTW 版本(恢复到最简单)
public void methodA() {
    System.out.println("进入 methodA");
    this.methodB(); // LTW 下,这个调用也会被拦截!
}

这个对比清晰地证明了 LTW 是如何从字节码层面根治自调用问题的。

模块 7:引介增强(Introduction)

引介增强(Introduction)或叫"混入"(Mixin),允许我们动态地让一组目标类"实现"一个新接口,并提供默认的实现逻辑。

7.1 使用方式:@DeclareParents

java 复制代码
@Aspect
@Component
public class AuditableAspect {
    // 1. value: 目标是哪些类,service包下的所有实现
    // 2. defaultImpl: 新接口的默认实现类
    @DeclareParents(value = "com.example.service.*+", defaultImpl = DefaultAuditable.class)
    private Auditable auditable; // 3. 新接口
}

// 新接口及默认实现
public interface Auditable {
    void audit(String operation);
}
public class DefaultAuditable implements Auditable {
    public void audit(String operation) {
        System.out.println("[Audit] 操作: " + operation);
    }
}

这样配置后,Spring 容器中所有匹配 value 表达式的 Bean,其代理对象都会额外实现 Auditable 接口。

7.2 底层原理:DelegatingIntroductionInterceptor

这个注解解析的背后,Spring 会创建一个 DelegatingIntroductionInterceptor,它同时实现了 IntroductionInterceptor 和 AOP 联盟的 MethodInterceptor 接口。

java 复制代码
// 代码来源: org.springframework.aop.support.DelegatingIntroductionInterceptor (简化版)
public class DelegatingIntroductionInterceptor extends IntroductionInfoSupport
      implements IntroductionInterceptor {

    private Object delegate; // 持有 defaultImpl 的实例

    public DelegatingIntroductionInterceptor(Object delegate) {
        this.delegate = delegate;
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        // 如果调用的是被引入的接口方法
        if (isMethodOnIntroducedInterface(mi)) {
            // 将调用委托给 defaultImpl 的实例
            return AopUtils.invokeJoinpointUsingReflection(this.delegate, mi.getMethod(), mi.getArguments());
        }
        // 否则,继续沿着原拦截器链执行
        return mi.proceed();
    }
}

源码解读:

  • 当一个被引介增强的代理对象调用 audit() 方法时,invoke 方法被触发。
  • isMethodOnIntroducedInterface(mi) 判断当前调用的方法是否属于被引入的新接口。
  • 如果是,它通过反射调用 delegate(也就是 defaultImpl 实例)的对应方法。
  • 如果不是,则通过 mi.proceed() 让调用继续沿着原本的拦截器链向下传递。

引介增强的代理结构类图:

classDiagram class TargetClass { +methodA() +methodB() } class Auditable { <> +audit() } class DefaultAuditable { +audit() } class CglibProxy { +methodA() (增强后) +methodB() (增强后) +audit() (从Auditable引入) } class DelegatingIntroductionInterceptor { -delegate: DefaultAuditable +invoke(MethodInvocation) } CglibProxy --|> TargetClass : 继承 CglibProxy ..|> Auditable : 动态实现 CglibProxy *-- DelegatingIntroductionInterceptor : 组合 DelegatingIntroductionInterceptor --> DefaultAuditable : 委托给

图表分层说明:

  • 主旨概括:该图展示了在 CGLIB 代理场景下,引介增强如何组织代理对象、拦截器与委托对象之间的关系。
  • 逐层分解
    1. 代理继承 :CGLIB 生成的 CglibProxy 继承了原始目标类 TargetClass
    2. 动态接口实现 :同时,CglibProxy 动态实现了我们指定的新接口 Auditable
    3. 拦截器组合CglibProxy 内部组合了 DelegatingIntroductionInterceptor
    4. 方法委托 :当 audit() 方法被调用时,DelegatingIntroductionInterceptor 将其委托给默认实现 DefaultAuditable 的实例。
  • 设计原理 :这是组合/委托 模式在 AOP 中的经典应用。DelegatingIntroductionInterceptor 作为一个特殊的拦截器,通过判断方法来源来分流调用,完美地将多个实现类的功能组合到一个代理对象上。
  • 工程联系与结论 :引介增强在需要为大量对象添加通用能力时非常有用。但在实践中,需要特别注意类型转换。如果你将一个被引介增强的原始 Bean 强转为 Auditable,在 CGLIB 代理下是可以的,因为代理类实现了 Auditable。但如果你获取的是未被代理的实例或代理剥离后的 TargetClass 实例,就会抛出 ClassCastException。这在多层代理嵌套的场景下尤其容易发生。

内联示例 5:引介增强使用与风险验证

测试代码 IntroductionTest.java

java 复制代码
@Test
public void testIntroduction() {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    // 从容器获取 Service,它是被增强的代理对象
    Object serviceBean = ctx.getBean("orderService");
    
    // 1. 验证代理
    System.out.println("Bean is proxy: " + AopUtils.isAopProxy(serviceBean));
    
    // 2. 尝试类型转换
    Auditable auditableService = null;
    try {
        // 如果代理成功实现接口,此转换应该成功
        auditableService = (Auditable) serviceBean;
        System.out.println("转型成功!Bean 类型: " + serviceBean.getClass().getName());
        auditableService.audit("创建订单操作");
    } catch (ClassCastException e) {
        System.out.println("转型失败!Bean 类型: " + serviceBean.getClass().getName());
    }
    
    // 3. 潜在风险:获取原始 target 对象
    if (AopUtils.isAopProxy(serviceBean)) {
        Object rawBean = AopProxyUtils.getSingletonTarget(serviceBean);
        System.out.println("原始 Bean 类型: " + rawBean.getClass().getName());
        // 尝试对原始 Bean 进行转型,必然失败
        boolean isAuditable = rawBean instanceof Auditable;
        System.out.println("原始 Bean 是否实现 Auditable: " + isAuditable); // 输出 false
    }
}

此测试验证了引介增强的成功,并指出了 getSingletonTarget 时可能发生的类型转换风险。

模块 8:AOP 的性能边界与最佳实践

AOP 是强大的抽象工具,但它不是免费的。理解其性能开销,是架构师做出正确决策的前提。

8.1 代理创建成本

每次 Spring 为 Bean 创建代理,都需要生成新的字节码并加载对应的类。

  • JDK 动态代理 :通过 java.lang.reflect.Proxy.newProxyInstance() 动态生成代理类。单次代理类创建成本低,因为它只代理接口,生成的类结构简单。但随着 JVM 运行,PermGen/Metaspace 会有一定占用。
  • CGLIB 代理 :通过 Enhancer 生成目标类的子类。代理类的创建成本相对较高,因为它需要解析目标类的所有 public 方法,并为每个方法生成重写逻辑。在 Bean 数量巨大(如微服务中数千个 Service)且全面启用 AOP 时,启动时间会显著增加。

8.2 拦截器链执行开销

java 复制代码
// ReflectiveMethodInvocation.proceed() 的简化逻辑
public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 链的末尾,调用目标方法
        return invokeJoinpoint();
    }
    Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        // 进行动态匹配,不匹配时递归调用 proceed() 跳过此拦截器
        if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        } else {
            return proceed(); // 跳过,但有递归开销
        }
    } else {
        // 静态匹配的拦截器,直接执行
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

开销分析: 每次方法调用,即使该 Bean 没有任何切面匹配,只要它是代理对象,就会进入 proceed() 的递归链。虽然无匹配时会快速返回,但调用栈的创建和销毁本身就是开销。在高频交易或高性能网关场景,这种纳秒级的损耗也可能成为瓶颈。

8.3 动态切点的性能陷阱

这是最危险的性能杀手。如模块 3 所述,当 MethodMatcher.isRuntime() 返回 true 时,InterceptAndDynamicMethodMatcher 会在每次方法调用时 都执行 methodMatcher.matches(...)。 例如,一个 @Around("args(java.lang.String, ..)") 将匹配所有第一个参数为 String 的方法。如果一个此类通知被错误地应用到一个高频调用的核心 Service 上,其参数匹配的开销将是惊人的。更糟的是,如果表达式本身复杂,如结合 @argsthis,性能损耗会成倍增加。

8.4 LTW 的特殊性能影响

  • 启动/首次加载延时:LTW 在类第一次被加载时进行织入,这会增加首次请求的延时。对于冷启动要求高的 Serverless 或频繁重启的测试环境,这可能是个问题。
  • 运行时性能 :织入完成后,LTW 的运行时性能通常优于 Spring AOP 动态代理。因为切面逻辑直接内嵌在方法体内,无需经过反射和拦截器链的调用,指令执行更紧凑,JIT 编译器也能更好地优化。

最佳实践总结:

  1. 精准表达式 :切点表达式尽量精确。能用 within 限定的范围,就不要用 execution 的全盘扫描。避免使用通配符 .. 指定过深的包路径。
  2. 慎用动态匹配 :除非业务逻辑强制要求运行时参数匹配(如根据参数值决定是否增强),否则优先使用 execution@annotation 等静态匹配表达式。
  3. 精简通知逻辑@Around@Before 等通知方法中的逻辑应极其轻量。任何耗时操作(如 I/O、复杂计算)都应异步化或移至外部系统。
  4. 监控代理数量:在 Spring Boot Actuator 或其他监控中,关注代理 Bean 的数量。数量异常增多可能意味着切面配置错误。
  5. 因地制宜
    • 业务代码:使用 Spring AOP 默认代理模式,平衡便利性与性能。
    • 中间件/框架:为追求极致性能和自调用支持,优先考虑 AspectJ LTW。
    • 核心高频模块:可以考虑手动代理,或直接使用 AspectJ 编译期织入来消除所有代理开销。

模块 9:生产事故排查专题

案例 1:动态切点匹配雪崩,CPU 飙升至 100%

  • 事故现象:某电商大促高峰期间,订单服务的 CPU 使用率突然飙升到 100%,服务假死。重启后问题依然迅速复现。

  • 排查思路

    1. top -H 找到最繁忙的线程 PID,转为 16 进制后,用 jstack 打印线程堆栈。

    2. 发现大量线程阻塞(或自旋)在 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed() 附近。

    3. 分析堆栈,发现调用链中存在一个 InterceptorAndDynamicMethodMatcher

    4. 审查该 Matcher 关联的切面,发现一个被错误放置的切面:

      java 复制代码
      @Around("args(String,..)")
      public Object logParams(ProceedingJoinPoint pjp) { ... }
  • 根因分析 :这个 args 表达式导致 MethodMatcher.isRuntime()true订单服务 中绝大部分方法的第一个参数都是 String 类型(如订单 ID、用户 ID)。这导致几乎所有方法调用都会进入动态匹配逻辑,进行不必要的参数类型判断。在高并发下,这种累积开销直接打垮了 CPU。

  • 解决方案 :将切点表达式精化,如 execution(* com..OrderService.create*(String,..)),或使用更精准的 @annotation 来标记需要记录日志的方法。

  • 最佳实践永远不要在任何面向大量 Bean 的切面中使用 args@args 等强动态匹配表达式,除非你明确知道代价并限定了极小的作用范围。 开启 Spring 的 TRACE 级别日志,可以辅助在测试环境发现 isRuntimetrueAdvisor

案例 2:LTW 与 CGLIB 冲突,Tomcat 下应用启动失败

  • 事故现象 :开发环境一切正常的 Spring 应用,部署到 Tomcat 容器后启动失败,抛出 IncompatibleClassChangeErrorClassFormatError
  • 排查思路
    1. 检查日志,发现异常与类加载和字节码修改相关。
    2. 检查新引入的依赖,发现最近为使用 AspectJ LTW,引入了 aspectjweaver.jar,并配置了 @EnableLoadTimeWeaving
    3. 检查 Tomcat 配置,发现 Tomcat 的 WEB-INF/lib 中同时存在 aspectjweavercglib
  • 根因分析 :Tomcat 的 WebappClassLoader 在加载类时,会使用 Instrumentation。启用的 InstrumentationLoadTimeWeaver 注册了 AspectJ 的 ClassFileTransformer,它会修改被加载的类。与此同时,Spring 也可能正在尝试用 CGLIB 为同一个类创建代理。AspectJ 的织入器(特别是 1.8.x 的 BCEL 版本)改变了类的结构,导致 CGLIB 后续再次读取这个已经被修改的类来生成子类时,发现类的结构与预期不符,从而抛出异常。两个字节码修改框架发生了冲突。
  • 解决方案
    1. 优先方案:检查织入范围。在 META-INF/aop.xml 中严格 <include within="..."/> 织入范围,排除掉那些 Spring 会使用 CGLIB 代理的 Bean(如事务代理的目标)。
    2. 次优方案:升级 AspectJ 版本到 1.9.x+,它使用 ASM 替代了 BCEL,兼容性更好。
    3. 终极方案:如果只是为了解决自调用,可以评估是否必须使用 LTW,是否可以用 AopContext 或架构重构替代。
  • 最佳实践 :在使用 LTW 时,务必精确定义织入范围,不要对整个项目进行全量织入。确保 aspectjweaver 和其他字节码工具库的版本兼容性。

案例 3:@DeclareParents 代理剥离导致 ClassCastException

  • 事故现象 :订单服务为所有 Service 通过 @DeclareParents 引入了 Auditable 接口。在某个 AOP 通知的 @AfterReturning 方法中,我们通过 JoinPoint.getTarget() 获取了目标对象,并尝试将其转为 Auditable 来记录审计日志。上线后,此处抛出了 ClassCastException
  • 排查思路
    1. 获取异常堆栈,定位到 (Auditable) joinPoint.getTarget() 这一行。
    2. 在这行代码前后添加日志,打印 joinPoint.getTarget().getClass().getName()
    3. 日志显示目标对象类名类似 com.example.service.OrderService,而不是代理类名(如 ...$$EnhancerByCGLIB$$...)。
  • 根因分析JoinPoint.getTarget() 返回的是被代理的真实目标对象 (the "naked" target),而不是经过层层包装的代理对象@DeclareParents 引入接口的实现是在代理层面完成的,并非修改了原始类。因此,原始 OrderService 类并不直接实现 Auditable 接口,强制转换必然失败。
  • 解决方案 :将 joinPoint.getTarget() 替换为 joinPoint.getThis()JoinPoint.getThis() 返回的是当前正在执行方法的代理对象 引用,即 CglibProxy 实例。这个代理对象当然实现了 Auditable 接口,转换是安全的。
  • 最佳实践 :在处理与 AOP 代理相关的类型转换时,务必清楚 getThis() (代理)和 getTarget() (原对象)的区别。任何通过 @DeclareParents 或其他代理增强获得的行为,都只能通过代理对象访问。

模块 10:面试高频专题

1. Spring AOP 和 AspectJ 是什么关系?各自的实现机制是什么?

  • 回答 :Spring AOP 是 Spring 框架提供的 AOP 实现,基于运行时动态代理 (JDK/CGLIB),它借用了 AspectJ 的注解体系和切点表达式语法 ,但默认不使用 AspectJ 的织入引擎。AspectJ 是一个完整的、独立的 AOP 框架,通过编译期、后编译期或加载期修改字节码实现织入,不依赖代理。
  • 追问 1 :Spring AOP 何时会"委托"给 AspectJ 工作?回答 :当配置了 LTW(加载期织入)时,Spring 通过 LoadTimeWeaver 将 AspectJ 的 ClassFileTransformer 注册到 JVM,后续的类加载过程就由 AspectJ 接管织入。
  • 追问 2 :既然 Spring AOP 用了 AspectJ 注解,为什么不在编译期用 ajc回答 :为了降低侵入性和复杂度。使用 ajc 会改变整个项目的编译方式,而 Spring 希望 AOP 能力可以无缝集成进标准 Java 构建流程。
  • 追问 3 :如何在一个项目中同时使用 Spring AOP 和 AspectJ?回答:可以,但需谨慎。通常业务切面用 Spring AOP 代理,基础设施切面(如事务、监控内部调用)使用 LTW 织入。需要严格控制 LTW 的织入范围,避免与 Spring CGLIB 代理冲突。
  • 加分回答 :详细阐述 AspectJExpressionPointcut 的编译缓存机制,说明为何它比原始字符串匹配高效。

2. 一个 @Aspect 类是如何被解析成多个 Advisor 的?BeanFactoryAspectJAdvisorsBuilder 的作用是什么?

  • 回答BeanFactoryAspectJAdvisorsBuilder 在容器启动时扫描所有 Bean,识别出 @Aspect 类,然后委托给 ReflectiveAspectJAdvisorFactory。该工厂遍历类中带有通知注解(@Before 等)的方法,为每个方法创建一个 InstantiationModelAwarePointcutAdvisorImpl,封装其切点和通知,最终生成 List<Advisor>
  • 追问 1InstantiationModelAwarePointcutAdvisorImplAdvisor 的关系?回答 :是接口和实现的关系。Advisor 是概念性接口,持有 AdvicePointcutAdvisor 增加了 Pointcut。而 InstantiationModelAwarePointcutAdvisorImpl 是其一个具体实现,延迟创建 Advice 实例。
  • 追问 2 :这个解析过程发生在 Bean 生命周期的哪个阶段?回答 :发生在 BeanFactoryPostProcessor 执行之后,BeanPostProcessor(特别是 AbstractAutoProxyCreator)开始处理之前。解析好的 Advisor 会被缓存,供后续创建代理时使用。
  • 追问 3 :同一个 Advisor 能匹配到不同 Bean 的不同方法吗?回答 :当然。Advisor 是通用的增强逻辑,其内部的 Pointcut 经过动态或静态匹配,可以应用于任何匹配成功的 Bean 的方法上。
  • 加分回答 :解释 LazySingletonAspectInstanceFactoryDecorator 的作用,即它如何保证 @Aspect 类的单例性在 Advisor 中也能得到保持。

3. 切点表达式 execution 和 within 的区别是什么?args 表达式有什么性能陷阱?

  • 回答execution方法级别 的最细粒度匹配,可以精确到返回类型、方法名和参数。within类级别 匹配,只能指定到类或包,它会匹配该类的所有方法。args 则是在运行时 匹配方法参数类型,这导致它强制启用 isRuntime=true 的动态匹配,每次方法调用都会进行参数判断,性能开销极大。
  • 追问 1 :如果我想拦截所有 get* 方法,用哪个表达式更好?回答 :用 execution(* get*(..)),它更精确。如果仅用 within(com..*) 会匹配大量无关方法,增加不必要的代理和匹配开销。
  • 追问 2 :写一个切点,同时拦截 Service 包下所有 public 方法,且被 @Transactional 注解的方法。回答execution(public * com.example.service..*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)
  • 追问 3@within(Transactional)@annotation(Transactional) 有什么区别?回答@within 要求注解在 上,它的目标是该类所有方法。@annotation 要求注解在方法上,它只作用于被直接注解的特定方法。
  • 加分回答 :从 AspectJExpressionPointcut 源码角度,解释其 matches(Method, Class)matches(Method, Class, Object...) 的调用时机。

4. 什么是 MethodMatcher?静态匹配和动态匹配的区别?isRuntime() 方法何时返回 true?

  • 回答MethodMatcher 是 Spring AOP 中用于判断一个方法是否被切点匹配的接口。其核心方法是 matches(Method, Class) 用于静态匹配 (启动时判断),和 boolean matches(Method, Class, Object... args) 用于动态匹配 (每次调用判断)。isRuntime() 是区分两者的开关,当表达式包含运行时才能确定的因素(如 args,通配的 @annotation 等)时返回 true
  • 追问 1 :如果一个切点组合了静态和动态表达式,isRuntime 返回什么?回答 :返回 true&& 运算中只要有一个是动态,整个切点都是动态的。
  • 追问 2 :静态匹配和 Advisor 的缓存有什么关系?回答 :静态匹配的结果可以被永久缓存,这样同一个 Advisor 对于同一个类的方法,只需要匹配一次。动态 Advisor 无法被完全缓存,每次调用前都可能需要重新判断。
  • 追问 3 :除了 args,还有哪些表达式会导致 isRuntimetrue回答@args@annotation 在绑定注解属性值到通知方法参数时、以及 thistarget 在某些复杂组合下都可能需要运行时匹配。
  • 加分回答 :从 AspectJExpressionPointcutmayNeedDynamicMatch() 方法调用讲起,解释其底层如何与 AspectJ 的 ShadowMatch 交互来判断是否需要动态匹配。

5. AspectJ 有哪几种织入方式?各自的特点和适用场景是什么?

  • 回答 :有三种。**编译期织入(CTW)**在 ajc 编译时修改源码,性能最好,但侵入构建过程。后编译期织入在编译后对 class/jar 文件进行织入,可以对第三方库织入。**加载期织入(LTW)**在 JVM 类加载时织入,最灵活,无需修改构建和源码,但有首次加载开销。
  • 追问 1 :为什么 Spring 选择深度集成 LTW,而非 CTW?回答 :因为 LTW 的延迟绑定非侵入性与 Spring 的哲学(不改变开发者的构建流程)最为契合。
  • 追问 2 :在一个已经使用了很多 Spring AOP 代理的项目中,能直接切换到 LTW 吗?回答:不能。需要移除大量代理相关配置,重写自调用相关代码,并且要严格处理 LTW 与 CGLIB 等字节码工具的兼容性。
  • 追问 3 :如何验证 LTW 织入成功?回答 :打印 bean.getClass().getName(),查看类名中是否包含 AspectJ 的特定后缀(如 $AjcClosure1)或不再包含 Spring 代理的 $$EnhancerByCGLIB$Proxy。亦可使用 -Daj.weaving.verbose=true 观察 JVM 输出。
  • 加分回答:结合实际案例,说明如何排查 LTW 在 Tomcat、JBoss 等不同容器中的类加载器隔离问题。

6. 加载期织入(LTW)的原理是什么?Spring 如何集成 LTW?

  • 回答 :原理基于 Java 5 的 java.lang.instrument API,通过注册 ClassFileTransformer,在 JVM 定义类之前修改其字节码。Spring 通过 LoadTimeWeaver 接口抽象不同环境的织入器获取方式,AspectJWeavingEnabler(一个 BeanFactoryPostProcessor)在启动时会使用 LoadTimeWeaver 将 AspectJ 的 ClassPreProcessorAgentAdapter 注册进去。
  • 追问 1InstrumentationLoadTimeWeaverReflectiveLoadTimeWeaver 的区别?回答 :前者直接使用 -javaagent 参数传递进来的 Instrumentation 实例,用于独立应用。后者通过反射调用容器的类加载器方法获取特殊的 ClassLoader,适用于 Web 容器环境。
  • 追问 2@EnableLoadTimeWeavingaspectjWeaving 属性有哪几个值?回答ENABLED(强制启用)、DISABLED(关闭)和 AUTODETECT(如果检测到 META-INF/aop.xmlaspectjweaver 依赖则自动启用)。
  • 追问 3 :如果没有 -javaagent 参数,也没有 Tomcat 特殊配置,仅靠 @EnableLoadTimeWeaving,LTW 能生效吗?回答 :不能。@EnableLoadTimeWeaving 只是配置了 Spring 上下文,要让 JVM 层面支持类转换,必须有 JVM Agent 或容器的 Instrumentation 支持。
  • 加分回答 :分析 AspectJWeavingEnabler.postProcessBeanFactory 的源码,说明其注册 transformer 的精确时机。

7. LTW 有哪几种配置方式?分别需要哪些依赖和启动参数?

  • 回答 :有三种。1) 注解方式@EnableLoadTimeWeaving 配合 -javaagent:spring-instrument.jar。2) aop.xml 方式 ,在 classpath 下创建 META-INF/aop.xml 定义织入规则,依赖于 JVM 层面的 LTW 支持(可能仍需要 agent 或容器)。3) 纯 JVM Agent 方式 ,使用 -javaagent:aspectjweaver.jar,完全脱离 Spring 配置。
  • 追问 1aop.xml 方式如何定义切面?回答 :通过 <aspects><aspect name="..."/></aspects> 元素声明切面类,通过 <weaver><include within="..."/></weaver> 定义织入范围。
  • 追问 2 :纯 -javaagent 方式需要 spring-instrument 吗?回答 :不需要。它直接使用 aspectjweaver.jar,绕过了 Spring,也意味着 Spring 相关的 Bean 切点(如 bean())会失效。
  • 追问 3 :在 Spring Boot 中,LTW 的配置有何不同?回答 :原理相同,但 Boot 的自动配置和嵌入式容器可能需要额外调整,例如需要配置 spring.jpa.properties.jakarta.persistence.validation.mode 等,并确保 spring-instrument 作为 agent 正确加载。
  • 加分回答 :指出 META-INF/aop-context.xmlaop.xml 的关系与区别。

8. 什么是自调用问题?为什么 Spring AOP 无法拦截内部调用?

  • 回答 :在代理模式下的 Bean 内部,通过 this.methodB() 发起的调用,this 指向的是原始目标对象而非其代理,从而导致调用绕过了所有 AOP 拦截器链。这是因为代理对象将外部调用转发给目标对象后,目标对象内部的调用是基于 Java 对象模型的直接引用。
  • 追问 1 :这是 Spring 的 Bug 吗?回答:不是。这是代理模式的固有特性,源于 Java 的对象模型。Spring 只是实现了代理模式。
  • 追问 2@Transactional 自调用失效是同一原因吗?回答 :完全一样。@Transactional 就是基于 AOP 实现的,所以内部调用 this.methodB() 会使得 methodB 上的事务注解失效。
  • 追问 3 :如果是嵌套的 @Service 调用 A 调用 this.methodB(),而 methodB 是 private 方法,CGLIB 代理能拦截吗?回答 :不能。CGLIB 通过生成子类和重写 public/protected 方法实现代理,private 方法无法被重写,当然也无法被拦截。
  • 加分回答:画出对象内存图,讲清楚代理对象和原始对象在堆中的引用关系。

9. 解决自调用问题有哪些方案?AopContext.currentProxy() 和 LTW 的优缺点各是什么?

  • 回答 :方案有:1) AopContext.currentProxy() 获取代理。2) 注入自身。3) 提取方法到另一个 Bean。4) 使用 AspectJ LTW。AopContext 方案优点是简单,缺点是代码侵入强、性能微损、且必须 exposeProxy=true。LTW 方案优点是根治,无代码侵入,缺点是需要 JVM 层面配置,有启动开销和兼容性风险。
  • 追问 1 :为什么说 AopContext 是"侵入性"的?回答 :因为它要求业务代码必须感知到 AOP 环境的存在,编写了 ((X)AopContext.currentProxy()).method() 这类的非业务代码,破坏了代码的纯粹性和可测试性。
  • 追问 2 :注入自身方案(@Autowired UserService self)在什么情况下会失败?回答 :在使用构造器注入 且没有 @Lazy 注解时,会直接产生 BeanCurrentlyInCreationException 循环依赖异常。
  • 追问 3 :LTW 方案对单元测试有何影响?回答:单元测试通常运行在简单的 JVM 环境,可能没有配置 Agent。因此,依赖于 LTW 的切面可能不会在测试中生效,需要为测试单独配置织入 Agent 或使用 Spring 的集成测试。
  • 加分回答:在实际项目中,提出"自调用问题决策树":方法是否需要事务?若是,能否重构到新类?若不能,优先级 LTW > AopContext。

10. 什么是引介增强(Introduction)?@DeclareParents 是如何工作的?

  • 回答 :引介增强允许动态地让目标类的代理实现一个新接口。@DeclareParents 定义目标类型范围(value)和默认实现(defaultImpl)。Spring 会为此生成一个 DelegatingIntroductionInterceptor,它会在代理拦截到新接口方法时,将调用委托给 defaultImpl 的实例。
  • 追问 1 :引介增强和普通切面增强有什么区别?回答 :普通增强是在现有方法前后添加逻辑(织入横切关注点),不改变类的类型层次。引介增强是让类实现一个新接口,扩展了其类型和能力。
  • 追问 2DelegatingIntroductionInterceptor 如何处理不是新接口的方法调用?回答 :通过 mi.proceed() 直接放行,让调用继续沿着原拦截器链向下传递。
  • 追问 3 :我能为一个类引入多个新接口吗?回答 :可以。通过定义多个 @DeclareParents 注解在不同的字段上即可。
  • 加分回答 :通过源码,展示 isMethodOnIntroducedInterface 是如何利用 MapIntroductionInfoSupport 中进行接口方法匹配的。

11. DelegatingIntroductionInterceptor 的作用是什么?引介增强在 JDK 和 CGLIB 代理下的表现有何不同?

  • 回答 :其作用是在拦截器链中充当新接口方法的分发器 。对于新引入的方法,委托给实现类;对于原方法,则放行。在 JDK 代理下,原始类与引入接口的实现都在同一个 $Proxy 类上,架构清晰。在 CGLIB 下,CGLIB 生成的子类直接 implements 了新接口,所有方法的调用都集中在子类上。
  • 追问 1 :哪种代理下更容易出现类型转换问题?回答 :CGLIB。因为多层代理嵌套时,最外层的 CGLIB 代理实现了接口,但通过 getTarget() 获取的内部 CGLIB 代理或原始对象可能没有。
  • 追问 2 :为什么说 JDK 代理更"纯粹"?回答:因为 JDK 动态代理是基于接口的,它天然地组合了多个接口的实现(业务接口+引介接口),不存在继承体系上的混淆。
  • 追问 3 :如何让 @DeclareParents 只对特定 Bean 生效?回答 :使用 bean 名称表达式,如 @DeclareParents(value = "com.example.service.* && bean(specificService)", ...)
  • 加分回答:结合 JMH 基准测试,说明 CGLIB 和 JDK 代理在引入增强时的微秒级性能差异。

12. Spring AOP 的性能开销主要来自哪些方面?如何优化?

  • 回答 :开销来自:1) 代理创建 (类生成与加载)。2) 拦截器链遍历 (每次调用的递归)。3) 动态匹配isRuntime=true 的参数检查)。4) 反射调用。优化方案:精化切点,尽可能静态匹配;减少不必要的通知;对性能敏感点使用 CTW 或手动代理。
  • 追问 1@Around@Before 哪个开销大?回答@Around 理论上开销稍大,因为它需要在内部显式调用 pjp.proceed(),调用栈更深一层。但实际差异极小,真正影响性能的是通知内的业务逻辑。
  • 追问 2 :如何监控 AOP 的性能影响?回答 :使用 APM 工具(如 SkyWalking, Pinpoint)通常能显示 AOP 层的耗时。也可以通过自定义 MethodInterceptor 并包装原始链来做基准测试。
  • 追问 3 :CGLIB 代理是否一定比 JDK 代理慢?回答:创建时 CGLIB 慢,但运行时方法调用,现代 CGLIB(ASM 生成索引调用)和 JDK 代理(反射)的性能差距已微乎其微。
  • 加分回答:用实际数据(如 Arthas trace 命令的输出)说明一个被 5 个动态 Advisor 拦截的方法调用,与无代理调用的耗时对比。

13. 动态切点(isRuntime = true)为什么会影响性能?如何排查这类性能问题?

  • 回答 :因为它迫使 Spring 在每次方法调用时都执行参数绑定和匹配逻辑,而静态匹配的结果可以被缓存并直接路由到拦截器。排查:使用 jstack 查看是否有线程反复执行 MethodMatcher.matches();审查所有切面的 @Pointcut 注解,找出使用 args, @args 等的位置;开启 Spring AOP 的 TRACE 日志,观察 Advisor 缓存行为。
  • 追问 1args 表达式一定慢吗?回答:如果作用范围极小(如只拦截一个特定方法),影响可以忽略。慢是相对的,量变引起质变。
  • 追问 2 :除了代码审查,有工具能自动检测出动态切点吗?回答 :理论上,可以通过一个 BeanPostProcessor 在启动时遍历所有 Advisor,并调用其 Pointcut.getMethodMatcher().isRuntime() 来收集和报警。
  • 追问 3 :一旦发现是动态切点导致的性能问题,如何快速止血?回答:最快速的止血方案不是改代码,而是通过配置或切面范围调整,将该切面从高频调用的模块中去掉,然后进行热更新代码。
  • 加分回答 :写一个 Spring 自定义的 DynamicPointcutDetector Bean,在启动后打印所有动态切点。

14. LTW 在生产环境中使用有哪些注意事项?它和 Spring Boot 的自动配置兼容性如何?

  • 回答 :注意事项:1) JVM Agent 配置 ,确保所有环境 (-dev, -prod) 都正确添加了 -javaagent。2) 织入范围精确 ,避免全局织入导致冲突。3) 依赖冲突 ,AspectJ 版本须与 spring-aspects 匹配。4) 监控启动时间 ,LTW 会增加首次类加载延迟。与 Spring Boot 兼容性良好,但需要将 spring-instrument 作为 Agent 路径提供给 Boot 的启动脚本。
  • 追问 1 :如何在 Docker 容器中部署需要 LTW 的 Spring Boot 应用?回答 :在 Dockerfile 中将 spring-instrument.jar 拷贝到镜像中,并在 ENTRYPOINTCMDjava 命令中加上 -javaagent 参数。
  • 追问 2META-INF/aop.xml 必须放在哪?回答 :必须放在 classpath 的根目录下(通常是 src/main/resources/META-INF/)。
  • 追问 3 :如果既有 @EnableLoadTimeWeaving 又有 aop.xml,谁优先?回答 :它们是相互配合的。@EnableLoadTimeWeaving 提供了 Spring 侧的 LTW 运行时环境,而 aop.xml 则是在此环境下定义的 AOP 织入规则的补充或替代来源。
  • 加分回答 :分享一个因忘记在 STAGING 环境添加 -javaagent 导致 LTW 静默失效,最终导致自调用事务未回滚的线上事故案例。

15. (系统设计题)设计一个全链路追踪系统,要求拦截所有 Service 方法的入参和出参,包括内部调用(如 A 方法调用 B 方法),并支持按 traceId 串联。请对比 Spring AOP + expose-proxy 方案与 AspectJ LTW 方案的优劣,并给出推荐的实现方式。

  • 回答
    • Spring AOP + expose-proxy 方案
      • 实现 :全局启用 exposeProxy=true。所有 Service 内部调用都必须显式写为 ((X)AopContext.currentProxy()).method()。依靠 MDC(Mapped Diagnostic Context)传递 traceId
      • 优点:纯 Spring 技术栈,理解和实现门槛低。
      • 劣点侵入性极强 ,所有开发人员必须时刻记住使用 currentProxy 来调用内部方法。代码丑陋,极易被遗忘导致追踪断裂。exposeProxy 本身有微小性能开销。对第三方库无效。
    • AspectJ LTW 方案
      • 实现 :配置编译期或加载期织入(推荐 LTW)。编写一个 Around 切面,拦截 execution(* com.example..*Service.*(..))。在通知中,从 MDC 获取或生成 traceId,记录入参,调用 pjp.proceed(),记录出参。内部调用因 this 本身已是增强对象,故能自动拦截。
      • 优点对业务代码零侵入。拦截无缝、完整,从根本上解决了内部调用丢失追踪的问题。
      • 劣点:需要 JVM 层面的 LTW 配置。对启动时间和首次调用有轻微影响。需要确保织入器与其它组件(ORM,Web 容器)兼容。
    • 推荐方案AspectJ LTW 方案 。对于全链路追踪这样的非功能横切关注点,应该与业务逻辑完全解耦。为了完整性、正确性和代码洁净度,LTW 带来的运维成本是完全值得的。这是让开发人员专注于业务,而非框架细节的唯一正确选择。
  • 追问 1 :如何设计 traceId 的生成和传递?回答 :在切面的最顶层入口(例如 MVC 拦截器)生成或从请求头提取 traceId 并放入 MDC。底层切面直接从 MDC 获取,避免在方法签名中显式传递。调用结束后,在 finally 块中清理 MDC,防止内存泄漏。
  • 追问 2 :如果你的 LTW 方案捕获了 @Async 异步方法,traceId 会怎么丢失?回答 :会丢失,因为 MDC 是线程绑定的。解决方案是让切面识别 @Async 方法,在调用前从 MDC 取出 traceId,并将其传递给 RunnableFuture,在子线程的 run 方法开始时重新放入 MDC。
  • 追问 3 :如何避免追踪系统本身成为性能瓶颈?回答 :1) 严格限制切点,只拦截 Service 层。2) 日志打印异步化,例如写入 RingBuffer。3) 使用高性能序列化。4) 采样追踪,而不是 100% 全量追踪。
  • 加分回答:提出一个更完善的方案,结合 Kubernetes 的 Sidecar 模式,在 Pod 级别通过 Network Proxy(如 Envoy)拦截流量,实现与语言无关的全链路追踪。而 Java 应用内部的 AOP 仅作为补充,用于捕获方法级的详细信息。

Demo 代码汇总与项目结构

以下汇总所有内联示例,形成可运行的 Maven 项目结构。

less 复制代码
aop-blog-examples/
├── pom.xml (父 POM,管理 Spring 5.x, AspectJ 1.9.x 依赖)
│
├── module-1-aspect-parsing/  (验证@Aspect解析)
│   ├── pom.xml
│   └── src/test/java/.../AspectParsingTest.java
│
├── module-2-pointcut-analysis/ (验证静态/动态切点)
│   ├── pom.xml
│   └── src/test/java/.../PointcutAnalysisTest.java
│
├── module-3-ltw-demo/ (验证三种 LTW 配置)
│   ├── pom.xml
│   └── src/
│       ├── main/java/...
│       │   ├── config/LtwConfig1.java (@EnableLTW 方式)
│       │   ├── service/MyLtwService.java
│       │   └── aspect/LtwXmlAspect.java (aop.xml方式用)
│       ├── main/resources/META-INF/aop.xml
│       └── test/java/...
│           ├── LtwAnnotationTest.java
│           └── LtwXmlTest.java
│
├── module-4-self-invocation/ (验证自调用及解决)
│   ├── pom.xml
│   └── src/test/java/...
│       ├── SelfInvocationTest.java
│       ├── config/ProxyConfig.java
│       └── config/LTWConfig.java
│
└── module-5-introduction/ (验证引介增强)
    ├── pom.xml
    └── src/test/java/...
        └── IntroductionTest.java

AOP 选型速查表

场景 推荐方案 使用方式 注意事项 对应模块
通用业务逻辑增强(日志、权限) Spring AOP (代理) @Aspect + @Component,默认代理模式。 仅能拦截 public 方法,需注意自调用问题。 模块 2, 3
接口实现动态扩展(多租户标识) @DeclareParents @DeclareParents 注解,代理会动态实现接口。 注意 getTarget() 的类型转换风险。CGLIB 代理为佳。 模块 7
内部方法调用增强(事务、追踪) (a) 架构重构 (b) AspectJ LTW (a) 拆分 Service (b) @EnableLoadTimeWeaving + JVM Agent (a) 合理但可能导致类爆炸 (b) 根治但需 JVM 配置,有启动开销。 模块 6, 模块 5
为第三方 JAR 添加横切逻辑 AspectJ LTW META-INF/aop.xml 配置织入规则 + LTW Agent 无源码修改,灵活性高。兼容性是最大挑战。 模块 5
高性能、非侵入性中间件开发 AspectJ CTW / LTW ajc 编译器编译,或 LTW 织入。 消除所有运行时代理开销,实现与业务代码完全解耦。 模块 4, 8
快速修复生产环境的特定问题 Spring AOP (代理) 编写一个精确的切面,紧急发版。 引入 Spring 依赖即可,无需改变 JVM 启动参数,部署简单。 模块 8, 9
全链路追踪/持续性能监控 AspectJ LTW 全局切面,从 MDC 获取 TraceId,无侵入。 必须处理异步线程的 MDC 传递,控制织入范围避免性能瓶颈。 模块 10 (Q15)

延伸阅读

  1. 《AspectJ in Action, Second Edition》 - Ramnivas Laddad
    • 全面了解 AspectJ 的权威书籍,包括了 LTW 原理与高级切点语法。
  2. 《Spring揭秘》 - 王福强
    • 其中 AOP 进阶章节对 Spring AOP 与 AspectJ 的关系、LTW 配置有透彻的讲解。
  3. Spring Framework Reference Documentation: Aspect-Oriented Programming
    • Spring 官方的 AOP 章节,详细描述了 @EnableLoadTimeWeavingaop.xml 等配置,是调试时的第一手资料。
  4. The AspectJ Programming Guide
    • AspectJ 官方编程指南(https://www.eclipse.org/aspectj/doc/released/progguide/index.html),涵盖了编译器 ajc、LTW 和全套语义。
  5. Java Instrumentation API 官方文档
    • 理解 Java 代理机制和 ClassFileTransformer 的基石。
  6. 《Java Performance: The Definitive Guide》 - Scott Oaks
    • 学习如何对 Spring 应用进行性能分析,特别是字节码生成和反射调用的开销评估,能帮助你量化 AOP 的性能影响。

本文从 @Aspect 注解的解析开始,横跨切点表达式的匹配原理、AspectJ 的三种织入方式、LTW 的核心机制,直指自调用问题的终极解法,并最终落地于性能最佳实践与生产事故复盘。我们不仅要知其然(如何使用),更要知其所以然(内部如何运作)和知其所限(在何处失效)。掌握了这些,才算真正迈过了 Spring AOP 的专家门槛,能够在架构演进中,充满信心地做出关于代理、织入和性能的每一项关键决策。

相关推荐
直奔標竿2 小时前
Java开发者AI转型第二十二课!Spring AI 个人知识库实战(一)——架构搭建与核心契约落地
java·人工智能·后端·spring·架构
曹牧3 小时前
Spring WebService 的两种主流实现方式‌
java·后端·spring
直奔標竿4 小时前
Java开发者AI转型第二十三课!Spring AI个人知识库实战(二):异步ETL流水线搭建与避坑指南
java·人工智能·spring boot·后端·spring
JAVA面经实录9174 小时前
Spring Boot + Spring AI 一体化实战全文档
java·人工智能·spring boot·spring
希望永不加班4 小时前
SpringBoot 接口签名验证(AppKey/Secret)
java·spring boot·后端·spring
曹牧6 小时前
Spring:@RequestMapping 注解匹配顺序
java·后端·spring
云烟成雨TD6 小时前
Spring AI Alibaba 1.x 系列【44】多智能体 - 混合模式、监督者(SupervisorAgent)、自定义模式
java·人工智能·spring
MaxCode-16 小时前
Chapter 9:企业实战案例与架构沉淀
人工智能·spring·架构
TE-茶叶蛋6 小时前
Spring自动配置分析
java·后端·spring