Spring Boot 3.2的AOT编译让我踩了个坑:启动快了,但反射全挂了。
上周把公司的一个内部工具升级到Spring Boot 3.2,顺手开了AOT编译,想试试GraalVM原生镜像。启动速度确实快了,但一跑反射代码全挂了。记录一下踩坑过程,供有同样计划的朋友参考。
什么是AOT编译
传统Java是JIT(Just-In-Time),运行时编译。AOT(Ahead-of-Time)是编译时就把代码转成机器码,运行时直接执行,省了JIT预热时间。
Spring Boot 3.2支持AOT,配合GraalVM原生镜像,启动速度提升明显,内存占用降低。听起来美好,但实际有坑。
我的项目背景
内部工具,功能不复杂:读取配置、连接数据库、执行定时任务、输出报表。用了Spring Boot 3.2 + MyBatis-Plus + PostgreSQL。
开AOT编译
<!-- pom.xml -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<imageName>report-tool</imageName>
</configuration>
</plugin>
执行 mvn spring-boot:process-aot 生成AOT元数据,再 mvn native:compile 编译原生镜像。
编译成功,启动时间从8秒降到1.2秒,内存从512MB降到180MB。爽。
一跑就挂
问题一:MyBatis-Plus的Mapper接口
@Mapper
public interface ReportMapper extends BaseMapper<Report> {
@Select("SELECT * FROM report WHERE create_time > #{startTime}")
List<Report> selectByTime(@Param("startTime") LocalDateTime startTime);
}
报错: MapperProxyFactory 找不到。MyBatis-Plus的Mapper接口是动态代理生成的,AOT编译时不知道哪些接口要生成代理。
解决:加反射配置
// reflect-config.json
{
"name": "com.example.mapper.ReportMapper",
"allDeclaredMethods": true
}
但项目有20个Mapper,手写不现实。用MyBatis-Plus的 @MapperScan ,AOT扫描不到。
最后方案:写了个脚本,编译前扫描所有Mapper接口,自动生成 reflect-config.json 。
问题二:Jackson的序列化
public class Report {
private LocalDateTime createTime;
// getter/setter
}
报错: JavaTimeModule 找不到。Jackson的 LocalDateTime 序列化用反射找 JavaTimeModule ,AOT编译时没注册。
解决:显式注册
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper;
}
但项目里还有自定义的 Mixin 、自定义的 Serializer ,全部要显式注册。排查了一下午。
问题三:配置属性绑定
@ConfigurationProperties(prefix = "report")
public class ReportProperties {
private String outputPath;
private List<String> emailReceivers;
}
报错: ConfigurationPropertiesBindingPostProcessor 找不到属性。AOT编译时, @ConfigurationProperties 的绑定用反射找setter方法,但AOT没生成。
解决:加 @Bindable
@Bindable
@ConfigurationProperties(prefix = "report")
public class ReportProperties {
// ...
}
但 @Bindable 是Spring Boot 3.2的新注解,文档里说"可选",实际AOT场景下是"必需"。
问题四:定时任务的Cron表达式
@Scheduled(cron = "${report.cron:0 0 1 * * ?}")
public void generateReport() {
// ...
}
报错: CronExpression 解析失败。AOT编译时, @Scheduled 的cron表达式是字符串,运行时解析,但AOT没生成解析代码。
解决:用固定表达式
@Scheduled(cron = "0 0 1 * * ?") // 不能用占位符
public void generateReport() {
// ...
}
或者配置类里动态注册:
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setCronExpression(reportProperties.getCron());
return scheduler;
}
但这样失去了 @Scheduled 的简洁性。
最终配置
reflect-config.json 里加了200多个类, proxy-config.json 里加了50多个接口,资源文件里加了10个配置文件。
编译时间从30秒变成12分钟,生成的原生镜像从45MB变成78MB(反射配置占空间)。
问题五:动态数据源
项目有动态数据源切换,根据用户选择切换不同数据库。AOT编译时,数据源是固定的,运行时切换报错。
解决:放弃动态数据源
改成固定数据源,或者启动时加载所有数据源配置。但这样失去了动态性,代码改动大。
问题六:日志配置
logback-spring.xml 里有 springProfile 动态配置,AOT编译时解析不了,运行时日志格式错乱。
解决:写死日志配置
去掉 springProfile ,写死 dev 和 prod 两套配置,编译时选一套。
现在的用法
内部工具,简单场景,AOT编译用着还行。但复杂项目(动态代理、反射多、配置复杂),AOT的维护成本太高。
我的结论
AOT编译适合:
无状态微服务
启动速度敏感(Serverless、K8s弹性伸缩)
反射少、配置简单的项目
不适合:
复杂企业应用(MyBatis、大量反射)
动态配置多的项目
需要频繁调试的项目(编译时间太长)
一句话总结
AOT编译是双刃剑,启动快了,但反射、动态代理、配置绑定都要显式处理。简单项目收益明显,复杂项目维护成本可能抵消收益。
最后说个槽点
Spring Boot 3.2的AOT文档,示例全是简单CRUD,没有MyBatis、没有复杂配置。我踩的坑,文档里一个都没提。官方说"生产就绪",实际"生产踩坑"。
更多推荐



所有评论(0)