Read OSS

预先编译(AOT):Spring 如何为原生镜像生成代码

高级

前置知识

  • 第 2 篇:BeanFactory 与 BeanDefinition 的核心概念
  • 第 3 篇:ConfigurationClassPostProcessor 与 refresh() 启动流程
  • 对 GraalVM 原生镜像约束的基本了解

预先编译(AOT):Spring 如何为原生镜像生成代码

纵观本系列,我们看到 Spring 在很大程度上依赖运行时的动态能力:第 2 篇中基于反射的 Bean 创建、第 3 篇中的注解扫描、第 4 篇中的动态代理生成。然而,这些技术对于 GraalVM 原生镜像来说都是障碍——原生镜像要求"封闭世界假设",即所有在运行时会用到的内容,必须在构建时就已确定。Spring Framework 的 AOT 引擎正是为了弥合这一鸿沟而生,它通过生成 Java 源代码,将动态行为替换为静态实现。本文将完整追踪这一流水线。

问题所在:原生镜像为何需要 AOT

GraalVM 的原生镜像编译器会激进地消除死代码,并在构建时预初始化类。为此,它需要提前知道:

  • 哪些类会被反射访问
  • 哪些资源会从 classpath 中加载
  • 哪些动态代理会被创建
  • 哪些序列化格式会被使用

第 3 篇中描述的传统 Spring 启动流程与上述要求相悖:ConfigurationClassPostProcessor 在运行时扫描 classpath,AbstractAutowireCapableBeanFactory 通过反射创建 Bean,DefaultAopProxyFactory 则动态生成 CGLIB 代理。

AotDetector 是连接两个世界的开关:

spring-core/src/main/java/org/springframework/aot/AotDetector.java#L31-L54

public static boolean useGeneratedArtifacts() {
    return (inNativeImage || SpringProperties.getFlag(AOT_ENABLED));
}
flowchart TD
    Start["Application Starts"]
    
    Start --> Check{"AotDetector<br/>.useGeneratedArtifacts()?"}
    
    Check -->|"false (JVM mode)"| Traditional["Traditional startup:<br/>classpath scanning,<br/>reflection-based bean creation,<br/>dynamic proxy generation"]
    
    Check -->|"true (native or AOT mode)"| AOT["AOT startup:<br/>load generated code,<br/>pre-computed bean definitions,<br/>pre-built proxy classes"]
    
    Traditional --> Runtime["Runtime: Full Spring flexibility"]
    AOT --> Native["Runtime: Faster startup,<br/>lower memory, no reflection"]

检测逻辑很简单:要么当前运行在 GraalVM 原生镜像中(通过 NativeDetector 检测),要么系统属性 spring.aot.enabled 已被设置。一旦 AOT 模式激活,生成的代码将完全取代动态配置处理流水线。

RuntimeHints:声明原生镜像所需的内容

并非所有的反射和资源访问都能被消除。某些框架和库在运行时确实需要它们。RuntimeHints 是 Spring 收集这些需求的注册中心:

spring-core/src/main/java/org/springframework/aot/hint/RuntimeHints.java#L34-L80

classDiagram
    class RuntimeHints {
        +reflection() ReflectionHints
        +resources() ResourceHints
        +proxies() ProxyHints
        +jni() ReflectionHints
    }
    
    class ReflectionHints {
        +registerType(Class, MemberCategory...)
        +registerConstructor(Constructor, ExecutableMode)
        +registerMethod(Method, ExecutableMode)
    }
    
    class ResourceHints {
        +registerPattern(String)
        +registerResourceBundle(String)
    }
    
    class ProxyHints {
        +registerJdkProxy(Class... interfaces)
    }
    
    RuntimeHints --> ReflectionHints
    RuntimeHints --> ResourceHints
    RuntimeHints --> ProxyHints

组件通过以下几种方式注册 hints:

  1. 实现 RuntimeHintsRegistrar —— 一个在 AOT 处理期间被调用的回调接口
  2. @Reflective@RegisterReflectionForBinding —— 用于声明反射需求的注解
  3. 通过 BeanRegistrationAotProcessor 自动注册 —— 分析 Bean 定义并自动注册 hints 的处理器

构建系统内置了验证机制。buildSrc 中的 RuntimeHintsAgentPlugin 配置了一个 Java agent,在测试期间运行,用于验证 hints 的完整性:

buildSrc/src/main/java/org/springframework/build/hint/RuntimeHintsAgentPlugin.java#L42-L60

标注了 RuntimeHintsTests 的测试会在该 agent 下运行——agent 会拦截反射调用,并验证对应的 hint 是否已被注册。如果某个反射调用没有对应的 hint 覆盖,测试就会失败。这将原生镜像的兼容性从"祈祷能跑"变成了可测试、可持续集成强制保障的工程实践。

AOT 处理流水线:从 Bean 定义到生成代码

AOT 流水线在构建时运行,处理的是与运行时相同的 Bean 定义。其核心接口是 BeanRegistrationAotProcessor

spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java#L50-L90

sequenceDiagram
    participant Build as Build Tool
    participant AOT as AOT Engine
    participant BRAP as BeanRegistrationAotProcessor
    participant Contrib as BeanRegistrationAotContribution
    participant GenCtx as GenerationContext
    participant ISCG as InstanceSupplierCodeGenerator
    participant JP as JavaPoet (vendored)

    Build->>AOT: Process application context
    AOT->>AOT: Simulate refresh() up to step 5
    Note right of AOT: Discovers all bean definitions<br/>without instantiating singletons
    
    loop For each RegisteredBean
        AOT->>BRAP: processAheadOfTime(registeredBean)
        BRAP-->>AOT: BeanRegistrationAotContribution
    end
    
    loop For each Contribution
        AOT->>Contrib: applyTo(generationContext, code)
        Contrib->>GenCtx: getGeneratedClasses()
        Contrib->>ISCG: generateCode(generatedMethods)
        ISCG->>JP: Build Java source with CodeBlock
        JP-->>ISCG: MethodSpec
        ISCG-->>Contrib: MethodReference
    end
    
    AOT->>GenCtx: Write all generated .java files
    Build->>Build: Compile generated sources

整个过程分为三个阶段:

第一阶段——发现:AOT 引擎模拟应用启动,执行 ConfigurationClassPostProcessor 和其他 BeanFactoryPostProcessor,发现所有 Bean 定义——这与第 3 篇中 refresh() 步骤 1 至 5 的过程完全一致。

第二阶段——生成贡献:对于每个发现的 Bean,引擎调用 BeanRegistrationAotProcessor.processAheadOfTime()。默认处理器会检查 Bean 定义的构造函数、工厂方法和属性值,并生成一个知道如何产生注册代码的 BeanRegistrationAotContribution

第三阶段——代码输出:每个 contribution 通过 GenerationContext(AOT 代码生成的核心接口)来输出代码:

spring-core/src/main/java/org/springframework/aot/generate/GenerationContext.java#L43-L57

GenerationContext 提供了三样东西:

  1. GeneratedClasses —— 用于创建新 Java 类的命名空间
  2. GeneratedFiles —— 用于写入资源文件、字节码或源代码
  3. RuntimeHints —— 用于注册反射/资源/代理 hints

InstanceSupplierCodeGenerator 是生成实际 Bean 实例化代码的核心:

spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java#L17-L59

它使用内嵌的 JavaPoet 库(位于 org.springframework.javapoet,即第 1 篇中提到的 shadow/repack 机制)来输出 Java 源代码。对于如下定义的 Bean:

@Bean
public MyService myService(MyRepository repo) {
    return new MyService(repo);
}

生成器会输出类似这样的代码:

public static MyService getMyServiceInstance(RegisteredBean registeredBean) {
    return new MyService(registeredBean.getBeanFactory().getBean(MyRepository.class));
}

没有反射,没有 Method.invoke(),只有 GraalVM 可以静态分析的直接构造函数调用。

BeanRegistrationsAotContribution 负责批量注册——它将单个 Bean 的注册逻辑分组到生成的类中,并以每个文件 5,000 条、每个方法 1,000 条为限进行拆分,以避免触碰 JVM 的限制:

spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java#L51-L58

提示: 想查看应用的 AOT 生成代码,可以运行 ./gradlew processAot(或 Maven 的对应命令),然后在 build/generated/aotSources 目录下查找。这些生成的类都是普通的 Java 代码——你可以直接阅读、设置断点,并用它们来调试 AOT 相关问题。

ConfigurationClassPostProcessor 的 AOT 集成

ConfigurationClassPostProcessor 不只是在运行时处理配置——它同样会参与 AOT 代码的生成。查看其 import 列表,就能看到这种双重身份:

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java#L44-L75

其中包含 GeneratedClassGeneratedMethodGenerationContextMethodReferenceRuntimeHintsReflectionHints——完整的 AOT 工具集。该类同时实现了 BeanRegistrationAotProcessorBeanFactoryInitializationAotProcessor,意味着它既可以在单个 Bean 层面,也可以在整个工厂层面贡献生成代码。

对于包含 @Bean 方法的 @Configuration 类,后处理器会生成直接调用工厂方法的代码,从而替代传统 @Configuration 类处理中依赖的 CGLIB 增强和反射方法调用。

BeanRegistrar 与 JSpecify:面向编译期安全的设计

第 3 篇中介绍的 BeanRegistrar API 在设计之初就将 AOT 视为一等公民:

spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java#L22-L41

flowchart LR
    subgraph "Traditional @Bean"
        T1["@Bean method"] --> T2["Reflection to discover"]
        T2 --> T3["Reflection to invoke"]
        T3 --> T4["AOT: Generate code<br/>to replace reflection"]
    end
    
    subgraph "BeanRegistrar"
        B1["register(registry, env)"] --> B2["Direct API calls"]
        B2 --> B3["supplier lambda"]
        B3 --> B4["AOT: Already<br/>reflection-free"]
    end

由于 BeanRegistrar 使用的是显式 API 调用和 supplier lambda,而非注解方法,AOT 引擎可以以最小的改造代价处理它。supplier(context -> new Bar(context.bean(Foo.class))) 这个 lambda 本身就是直接的构造函数调用,无需替换任何反射。

BeanRegistrar 同步推进的,是 Spring 7.x 将空值注解从 org.springframework.lang.@Nullable 迁移至 JSpecify 的 org.jspecify.annotations.@NullableNullness 工具类为此提供了支持:

spring-core/src/main/java/org/springframework/core/Nullness.java#L43-L60

这次迁移的意义不只是改善 IDE 支持。JSpecify 注解专为静态分析工具和编译器而设计,能够在编译期检测空值安全违规。对于 AOT 而言,这意味着代码生成器可以对可空性做出更强的假设,从而减少运行时检查。第 1 篇中提到的 ArchUnit 规则通过禁止 org.springframework.lang 下的旧 @Nullable@NonNull import,强制执行这一迁移。

flowchart TD
    subgraph "Spring's Compile-Time Safety Strategy"
        JSpecify["JSpecify @Nullable / @NullMarked"]
        BR["BeanRegistrar (no reflection)"]
        AOT["AOT Code Generation"]
        ArchUnit["ArchUnit enforcement"]
        
        JSpecify --> |"Enables"| StaticAnalysis["Compile-time null checks"]
        BR --> |"Enables"| NoReflect["Reflection-free bean creation"]
        AOT --> |"Produces"| Generated["Generated Java source"]
        ArchUnit --> |"Prevents"| OldAPI["Use of deprecated annotations"]
        
        StaticAnalysis --> Native["GraalVM Native Image"]
        NoReflect --> Native
        Generated --> Native
    end

全局视角:从动态到静态

回顾本系列的全部内容,AOT 的故事将每一个核心组件串联在一起:

  1. 第 1 篇(构建系统):spring-core 中内嵌的 JavaPoet 库是代码生成引擎,RuntimeHintsAgentPlugin 在 CI 中验证 hints 的完整性。

  2. 第 2 篇(IoC 容器):基于 createBeanInstance() 和反射的 Bean 创建流程,被生成的 InstanceSupplier 代码所取代,后者直接调用构造函数。

  3. 第 3 篇(refresh() 与配置)ConfigurationClassPostProcessor 在运行时扫描 classpath 的行为,被预先计算好并写入生成代码的 Bean 定义所取代。BeanRegistrar 则提供了一种天然契合 AOT 的替代方案。

  4. 第 4 篇(AOP 代理):CGLIB 代理类在构建时生成并打包进原生镜像,而非在运行时动态生成。

  5. 第 5 篇(Web 分发):WebFlux 的函数式 RouterFunction 模型天然对 AOT 友好,因为路由是在代码中定义的,而非通过注解扫描发现。

AOT 引擎不只是一个优化手段——它是一次根本性的架构转变,使 Spring 丰富的动态编程模型得以在禁止动态行为的环境中运行。生成的代码是开发者体验(注解、扫描、代理)与运行时效率(静态分发、无反射、极速启动)之间的桥梁。

提示: 调试 AOT 问题时,最常见的原因是缺失 RuntimeHints。如果某个 Bean 在运行时需要反射(例如,被 Hibernate 内省的 JPA 实体),请确保为该类型注册了 RuntimeHintsRegistrar。使用 @ImportRuntimeHints 注解可以让这一过程更加声明式。

系列总结

在这六篇文章中,我们走遍了 Spring Framework 的每一个角落——从包含 22 个模块的 Gradle 单体仓库和构建规范,到 IoC 容器的接口层级与 Bean 创建流水线,再到启动流程与配置处理,穿越 AOP 代理基础设施,深入两种 Web 分发架构,最终来到 AOT 编译引擎。每一层都建立在其下层之上,每一个架构决策背后的原因,只有在理解了全局之后才会清晰呈现。

Spring Framework 经久不衰的成功,源于其内在的一致性:相同的模式(模板方法、策略模式、接口隔离)贯穿每一个层级,使得即便面对 150 万行的代码库,也依然有迹可循。完善的 Javadoc 让源代码本身成为最好的参考资料——而现在,你已经知道该去哪里寻找答案了。