人人都会AI编程

5.5 切面织入时机:编译期、类加载期、运行期

更新时间:2026-07-11

切面织入(Weaving)是将切面逻辑应用到目标对象的过程。根据织入发生的时机不同,可以分为编译期织入、类加载期织入和运行期织入三种类型。理解它们的区别,有助于在实际项目中根据性能要求、部署环境和工具支持做出合理选择。

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

编译期织入是在源码编译为字节码的阶段,由特殊的编译器(通常是 AspectJ 编译器 ajc)将切面代码直接织入目标类中。编译完成后,生成的 .class 文件已经包含了增强后的逻辑,运行时不再需要任何代理或额外处理。

工作方式

  • 使用 AspectJ 的 ajc 替代标准 javac 编译源代码。
  • 或者在 Maven/Gradle 构建中集成 AspectJ 插件(如 aspectj-maven-plugin),在编译期自动完成织入。
  • 切面定义通常使用 AspectJ 的注解(@Aspect)或原生语法(.aj 文件),目标类无需实现接口。

示例

一个简单的统计方法执行时间的切面:

@Aspect
public class TimingAspect {
    @Around("execution(* com.example.service.*.*(..))")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        Object result = pjp.proceed();
        long end = System.nanoTime();
        System.out.println(pjp.getSignature() + " 耗时: " + (end - start) + " ns");
        return result;
    }
}

在 Maven 中配置 aspectj-maven-plugin,使用 ajc 编译:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.14.0</version>
    <configuration>
        <complianceLevel>1.8</complianceLevel>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
    <executions>
        <execution>
            <goals><goal>compile</goal></goals>
        </execution>
    </executions>
</plugin>

编译后,UserService.class 的字节码中就已经包含了计时逻辑,运行时就是增强后的版本。

优点

  • 无运行时开销:不需要动态代理,没有反射调用,性能最优。
  • 可织入任何类:不限于 Spring 管理的 Bean,可以对普通的 POJO、静态方法、构造器等进行增强。
  • 全面支持细粒度连接点:包括字段访问、异常抛出、初始化块等,Spring AOP 不支持。

缺点

  • 编译过程依赖特殊编译器:需要引入 ajc 或构建插件,对团队构建流程有侵入。
  • 调试体验稍差:由于源码和编译后的类逻辑不同,堆栈信息可能不够直观。
  • 不能动态调整切面:切面逻辑在编译时已固化,无法按环境启用或禁用。

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

类加载期织入指的是在 JVM 加载 .class 文件并定义类时,通过类加载器代理(Weaving ClassLoader)Java 代理(Java Agent) 将切面织入。与编译期织入一样,它生成的字节码已包含增强,但时机推迟到类被加载期间。

工作方式

最常见的方式是使用 Java Agent。在 JVM 启动时通过 -javaagent 参数指定 AspectJ 的织入器(aspectjweaver.jar),它可以拦截所有类的加载过程,根据 aop.xml 配置决定对哪些类进行织入。

配置步骤

  1. META-INF/aop.xml 中定义切面和目标:
<aspectj>
    <aspects>
        <aspect name="com.example.aspect.TimingAspect"/>
    </aspects>
    <weaver options="-verbose -showWeaveInfo">
        <include within="com.example.service.*"/>
    </weaver>
</aspectj>
  1. 启动应用时添加 Java Agent:
java -javaagent:path/to/aspectjweaver.jar -jar myapp.jar

Spring 框架也内建了 LTW 支持。在 Spring Boot 应用中,可以通过 @EnableLoadTimeWeaving 注解开启:

@Configuration
@EnableLoadTimeWeaving(aspectjWeaving = ENABLED)
public class AppConfig { }

但这种方式要求容器使用支持织入的类加载器(如在 Tomcat 中需配置 spring-instrument 作为 Java Agent),并不像编译期织入那么直接。

优点

  • 几乎与编译期织入相同的运行时性能:字节码已增强,无需反射代理。
  • 切面可独立于业务代码维护:可以在不重新编译应用的情况下,通过 agent 和 aop.xml 配置切面,甚至在不同环境灵活启用。
  • 不污染源码和编译流程:业务代码的编译过程保持纯 Java 标准。

缺点

  • 需要 Java Agent 或定制类加载器:增加了运维的复杂度,尤其是在容器化环境中需牢记添加 agent 参数。
  • 启动时间稍长:织入过程在类加载时进行,第一次加载类可能有轻微延迟。
  • 调试较复杂:堆栈与源文件不完全对应。

5.5.3 运行期织入(Runtime Weaving)

运行期织入是 AOP 框架在程序运行期间,通过动态代理字节码生成(如 CGLIB)来创建增强对象的。Spring AOP 正是采用这种方式,也是绝大多数 Java 开发者最熟悉的织入时机。

工作方式

  • 如果目标对象实现了接口:Spring 默认使用 JDK 动态代理,产生一个实现相同接口的代理对象。
  • 如果目标对象未实现接口:Spring 使用 CGLIB 动态字节码技术,生成目标类的子类作为代理。
  • 代理对象在调用目标方法时,会先执行切面逻辑(通知链),再通过反射调用原始方法。

整个过程完全在 JVM 运行期动态完成,无需修改编译过程或启动参数。

示例

Spring 中只需声明一个 @Aspect 并交给容器管理,即可在运行期织入:

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("调用方法: " + joinPoint.getSignature());
    }
}

容器自动为匹配的 Bean 创建代理,每当这些 Bean 的方法被调用时,切面逻辑自动触发。

优点

  • 部署极简:不需要特殊编译器或 Java Agent,标准 JVM 即可运行。
  • 与 Spring 深度集成:自动配置、作用域、依赖注入等都能无缝协作。
  • 动态可控:可以基于 @Profile@Conditional 等动态启用或禁用切面,灵活性高。

缺点

  • 性能开销:方法调用需要经过代理层(反射与代理链),但对于绝大多数企业应用,这部分开销可忽略。
  • 联接点受限:Spring AOP 仅支持方法级别的拦截,不能作用于字段访问、构造器等。
  • 自调用失效:当目标类内部方法调用时(this.method2()),由于绕过了代理,切面不会生效。这是 Spring AOP 中常见的“代理陷阱”,需要特别注意。
  • 必须通过容器获取 Bean:只有由 Spring 管理的 Bean 才能被代理,不能对普通的 new 对象切面。

5.5.4 三种织入时机的对比与选择

| 特性 | 编译期织入 (CTW) | 类加载期织入 (LTW) | 运行期织入 (Spring AOP) |
|------------------------|-----------------------|---------------------------|--------------------------|
| 织入时机 | 编译源代码时 | JVM 加载类时 | Bean 初始化前创建代理时 |
| 技术实现 | ajc 编译器 | Java Agent / ClassLoader | JDK 动态代理 / CGLIB |
| 性能 | 最佳,无运行时开销 | 接近 CTW,加载时一次性开销| 有额外反射和调用链开销 |
| 连接点支持 | 全面(方法、字段、构造器、异常等)| 与 CTW 相同 | 仅方法执行 |
| 是否需修改构建流程 | 是(引入 ajc) | 否(但需添加 agent) | 否 |
| 能否对非 Spring Bean 增强| 是 | 是 | 否 |
| 调试友好度 | 较低(字节码不同) | 较低(源码与增强分离) | 高(堆栈与源码一致) |
| 灵活性与动态控制 | 低(编译即固化) | 中(通过 agent 和配置文件控制)| 高(Spring 环境灵活配置)|

5.5.5 实际选型指南

对于绝大多数基于 Spring 的应用,运行期织入(Spring AOP) 是最自然的选择。它部署简单、集成度高,且能覆盖 95% 的横切需求(事务、安全、日志、缓存等)。只有在以下场景才需要考虑编译期或类加载期织入:

  • 需要极致的性能和最低延迟,且用到了大量切面(如高频交易系统),运行期代理的开销无法接受。
  • 需要增强不由 Spring 管理的类,例如领域对象、遗留代码或第三方库。
  • 需要拦截字段访问、构造器执行或静态方法
  • 已经大量使用 AspectJ 原生语法,并希望统一使用一种切面技术。

如果选择了 AspectJ 的编译期或类加载期织入,仍然可以与 Spring 集成——Spring 支持将 AspectJ 切面注册为 Bean,并利用 Spring 的依赖注入为其装配依赖。

总之,理解织入时机的本质差异,能帮助你在系统设计时做出权衡,而不是盲目使用“更强大”的 AspectJ。绝大多数时候,Spring AOP 已经足够“明了、实用、真实”地解决实际问题。