深入 Spring 42:AOT 生成代码与运行时配置的边界
启动参数出现 red,容器里的 Bean 仍然是 blue
配置类定义两个同类型 Bean:@Profile("blue") 返回 blue,@Profile("red") 返回 red。普通 JVM 分别带 blue、red 启动,容器里也分别出现对应 Bean。用 blue 生成 Spring AOT 产物后,再给 AOT JVM 传 red,实验仍取得 blue Bean。
日志中有效 profile 是 blue,red,这并不矛盾。环境属性仍可在启动时参与处理,已经生成的 Bean 注册代码却不会重新执行一次任意的配置类发现与条件选择。把“Environment 中出现了某个值”当作“容器根据该值重建了 Bean 集合”,会误判 AOT 应用的装配行为。
实验固定 Framework 6.2.11、Boot 3.5.6 与 JDK 21。final-lab/AotApp 是独立的非 Web 入口,避免把数据库连接和服务器启动引入 AOT 基础实验;它与订单入口共享 Maven 模块和冻结依赖。普通 JVM、生成代码、AOT JVM 均已实际执行,native image 单独标记为 NOT_RUN。
Spring AOT 提前执行的是容器装配决策
普通容器启动需要解析配置类、处理导入、识别 Bean 方法、评估条件并注册 BeanDefinition。Spring AOT 将其中可在构建阶段确定的部分转成生成代码。运行时可以直接应用这些注册结果,减少重复发现与分析。
固定版本 ApplicationContextAotGenerator.processAheadOfTime 首先调用 refreshForAotProcessing,再建立初始化代码生成器,收集 BeanFactory 初始化阶段的 AOT 贡献。返回值指向生成的 ApplicationContextInitializer 类型。它描述如何恢复优化后的上下文初始状态,而不是把正在运行的整个 JVM 内存保存成快照。固定版本生成器源码
构建时刷新也不等于完整业务启动。其主要目标是 Bean 定义和可分析元数据;AOT 处理器、相关基础设施及其必要依赖可能实例化,普通业务 Bean 不应被假设为已经全部运行过初始化逻辑。因此,构建阶段能成功生成代码,不能证明连接池取得了连接、远端接口可访问或关闭回调会正确释放资源。
声明类型也会影响分析能力。@Bean 返回一个过于宽泛的 Object,而实际创建某个带注解、代理接口或生命周期能力的具体类型,会让构建分析缺少信息。对于需要 AOT 分析的注册,保留可解析的 BeanDefinition 与具体返回类型,比在运行过程中任意注册已创建单例更容易表达完整语义。AOT 容器约束
本地生成的文件究竟包含什么
本例 Maven 插件显式指定 AotApp 为 mainClass,构建 profile 固定为 blue。执行 spring-boot:process-aot 后,target/spring-aot/main/sources 出现 AotApp__ApplicationContextInitializer、AotApp__BeanFactoryRegistrations 与 AotApp__BeanDefinitions 等源码。
初始化器负责把生成的工厂注册逻辑应用到 GenericApplicationContext。BeanDefinitions 中保留用于创建 blue Bean 的定义;BeanFactoryRegistrations 将这些定义放进 BeanFactory。检查源码可以看到注册路径,但还需要编译并运行相同产物,才能确认这些类真的进入最终应用包并参与启动。
实验先运行普通 jar,再执行 AOT 处理与 package,最后用 -Dspring.aot.enabled=true 运行重新打包的 jar。缺少这个开关时,应用仍可能走普通启动路径,单凭目录里存在生成文件不能证明 AOT JVM 已运行。日志明确打印 AOT_ENABLED=true,同时对 Bean、资源和反射结果进行断言。
生成的 Java、class 与 native metadata 被复制到 evidence/42/local-20261002/generated/;最终 jar 保存在模块 target 中,证据记录其 SHA-256。生成目录是观测对象,修改业务配置时应该重新生成,不能直接手工修生成代码后把它当作源代码修复。
profile 决定 Bean 集合,普通属性仍有运行空间
blue 与 red 是两个互斥的装配选择:它们决定注册哪个 Marker Bean。普通 JVM 每次启动都会重新处理这个选择,因此四次运行中的前两次分别得到 blue、red。AOT 构建时使用 blue,后两次运行则都得到 blue。
后一次 AOT 运行仍传入 --spring.profiles.active=red。Boot 生成的环境处理逻辑保留构建所需 profile,实际日志出现 blue,red。实验只据此说明当前冻结版本和配置组合的观测结果;不能把该字符串推广为所有 Spring AOT 应用都会采用的 profile 合并规则。
同一次运行还传入 --lab.expected=blue,主程序通过 Environment 读取该属性作为断言的期待值。该属性不改变 Bean 是否存在,因此可以在运行时取得。对数据库地址、业务阈值等属性,也应分别检查它们只是改变已存在对象的配置,还是影响条件装配、类型或 Bean 数量。
@ConditionalOnProperty 一旦决定某个 Bean 是否注册,就属于后者。把这类属性留到部署时才决定,而构建阶段没有对应 Bean 定义,运行时不能期待它自动补出整套装配。环境配置需要按“影响对象值”与“影响对象集合”逐项判断,不能统称为配置都被冻结或配置都可变。Boot 的 AOT JVM 运行限制
RuntimeHints 表达动态访问需求
静态分析不能自动知道每次反射、资源查找或动态代理最终会使用哪些目标。RuntimeHints 为这些操作提供元数据。它不是允许任意反射的全局开关,也不是应用运行时替业务代码执行反射的工具。
本例 Hints 注册 lab-aot-message.txt 资源模式,并为 Payload 注册公开构造器和公开方法调用需求。应用实际通过 ClassPathResource 打开资源,通过 getConstructor().newInstance() 创建对象,再反射调用 value 方法。四次 JVM 运行都断言返回 resource-ok 和 reflection-ok。
构建生成的 resource-config.json 与 reflect-config.json 中也分别包含资源名和 Payload 类型。这个检查证明注册器参与生成且相关声明写入了输出;它没有证明这些声明已经覆盖应用中所有动态访问,也没有证明删除 hints 会让普通 JVM 失败。
普通 JVM 本身具备反射与 classpath 资源能力,缺失 native metadata 通常不会让这里的 JVM 演示立刻失败。即使 AOT JVM 成功执行,也不能据此声称 native image 的封闭世界分析已经通过。两种运行方式的限制不同,证据必须分开保存。RuntimeHints 固定接口
三种产物的验收记录
| 对象 | 实际操作 | 已观察结果 | 未包含的证明 |
|---|---|---|---|
| 普通 JVM jar | blue、red 分别启动 | Bean 随 profile 改变,资源和反射成功 | AOT 初始化器是否可用 |
| AOT JVM jar | 生成、编译、打包后启用 AOT | 两次均为 blue,生成元数据存在 | native 封闭世界分析与本地机器码行为 |
| native image | 当前工具探测 | PATH 与选定 JDK 无 native-image,NOT_RUN | 构建、启动、资源、反射及性能全部未验收 |
本例共有 18 条具名检查:四次实际 JVM 运行各检查四项,另检查生成 Java 与 hints 输出两项。没有测量冷启动分布、峰值内存或吞吐量,日志中的单次启动耗时不构成性能比较。生成源代码也不意味着 Java JIT 被禁用;AOT JVM 仍然运行在 JVM 上。
native image 工具缺失是本次明确的验证边界。不能从当前 JVM 结果推断 native 的二进制可运行,更不能把 native 的构建失败预先归因于某个 hint。获得工具后仍需执行实际构建、运行和资源反射检查,保留新的产物与日志。
运行命令与修改练习
从系列实验工程根目录执行:
1 | |
脚本先 package 普通 jar 并运行两次,再 process-aot、package,最后运行两次 AOT JVM。每个命令单独保留输出和退出码,合并日志位于 evidence/42/local-20261002/run.txt。AOT 步骤的核心命令可以独立复现:
1 | |
把 pom 中构建 profile 从 blue 改为 red,清理本模块 target 后重新生成,再把两个 AOT 运行的 lab.expected 改为 red。预期生成代码注册 red,运行期传 blue 也不会把定义重新切成 blue。该修改练习未包含在保存的基线运行中;只改启动参数而不重新构建,是另一种实验。
还可以删除 Hints 中资源注册行后重新生成,检查 resource-config 是否缺少该资源模式。普通 JVM 仍可能读取成功,这正是“JVM 可运行”与“native 元数据足够”之间的差别。此练习不要求凭空宣称 native 失败,缺少实际原生产物时结论应停在生成 metadata 的变化。

