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<Advisor> 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字节码<br/>含织入逻辑]; end subgraph 后编译期织入 Binary Weaving direction LR B1[.class字节码] --> B2{织入工具}; B2 --> B3[.class字节码<br/>含织入逻辑]; 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 { <<interface>> +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 的专家门槛,能够在架构演进中,充满信心地做出关于代理、织入和性能的每一项关键决策。

相关推荐
咖啡八杯14 小时前
GoF设计模式——备忘录模式
java·后端·spring·设计模式
Flittly2 天前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
咖啡八杯3 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
Flittly4 天前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
唐青枫8 天前
Java Spring WebFlux 实战指南:用 Mono、Flux 和 WebClient 写响应式接口
java·spring
咖啡八杯10 天前
GoF设计模式——策略模式
java·后端·spring·设计模式
Flittly11 天前
【AgentScope Java新手村系列】(11)中断与恢复
java·spring boot·spring
dunky11 天前
Spring 的三级缓存与循环依赖
后端·spring
码云数智-园园16 天前
C++20 Modules 模块详解
java·开发语言·spring
咖啡八杯16 天前
GoF设计模式——享元模式
java·spring·设计模式·享元模式