Solon AOT & Native:三段式编译,把 Java 原生编译的门槛从珠穆朗玛峰削到崇明岛
写在前面
Java 程序员写原生可执行文件,这条路从来就没好走过。
GraalVM Native Image 是个好技术,但它要求你熟悉 @RegistrationGent、理解 HSF、了解 native-image-agent,还得祈祷你依赖的那个第三方库没有用反射、没有用 Class.forName、没有在运行时搞字节码增强。这些东西凑在一起,就把「写 Java、跑 native」这件事的门槛,推到了珠穆朗玛峰的高度。
Solon AOT & Native 做的事情,就是把这个门槛削掉。它发明了一套三段式编译流水线,把 GraalVM Native Image 的适配工作,从手动模式变成了自动模式——你几乎感知不到 GraalVM 的存在,写 Java 代码,打包,原生二进制就出来了。
这篇文章,我们从 Java 原生编译的历史账开始算,摸清楚 Solon 的三段式流水线到底是怎么工作的,最后用真实项目跑通从写代码到产出原生二进制的全链路。
一、Java 原生编译的历史债:为什么这条路一直很难走
1.1 JVM 的运行时魔法
Java 之所以在 90 年代崛起,一个核心原因是「Write Once, Run Anywhere」——写一段代码,JVM 负责在各种操作系统和 CPU 架构上运行。但这份便利是有代价的:
- JVM 启动慢:先加载字节码解释器,JIT 编译器在运行一段时间后才能发挥效果,冷启动往往是几百毫秒到几秒
- 内存占用高:堆、栈、元空间、JIT 编译器本身都要消耗内存,一个简单的 Spring Boot 应用动不动就要 256MB+ 的堆
- 打包体积大:JVM 运行时本身就是几十到上百 MB,再加上应用 JAR,打出来的部署包轻轻松松几百 MB
这些问题在云原生时代被无限放大——Serverless 函数要求毫秒级冷启动,容器镜像要求尽量小,打包部署要求简单粗暴。
GraalVM Native Image 正是为了解决这些问题而生的。它的核心原理是 AOT(Ahead-of-Time)编译:在构建阶段就把 Java 字节码编译成本地机器码,同时通过「静态分析」推断哪些类、哪些方法在运行时是真正需要的,把不需要的全部裁掉。
最终产出是一个独立的原生可执行文件,不需要 JVM 在运行时参与。
1.2 GraalVM Native Image 的现实问题
理想很丰满,现实是:GraalVM Native Image 的适配工作极度繁琐。
GraalVM 的静态分析是保守的。它会把所有的反射调用、动态代理、ServiceLoader 加载、Class.forName 都当成「可能在运行时用到」来处理。问题是,Java 生态里大量框架都依赖这些动态特性:
- Spring 大量使用 @Component、@Autowired 等注解,启动时扫描 classpath
- MyBatis 用动态代理生成 Mapper 实现
- Jackson 在序列化时动态发现字段
- 各类 ORM 框架 用反射读写字段
如果你直接对这类项目跑 native-image,十有八九会得到一个能编译但跑不起来的二进制——运行时找不到类、字段、找不到方法。
解决方案是用 native-image-agent 做「录制」:先以 JVM 模式正常运行应用,让 agent 监听并记录所有的反射、动态代理、Resource 加载行为,生成 reflect-config.json、proxy-config.json、resource-config.json,然后把这些配置文件喂给 native-image。
但这套流程有几个问题:
- 覆盖率难保证:录制时跑的路径不一定覆盖了所有运行场景,漏掉的路径在生产环境就炸了
- 版本漂移:依赖升级后,之前录制的配置文件可能不再适用
- 调试困难:出了问题是 AOT 分析的锅、还是配置的锅、还是代码本身的锅,很难定位
- 门槛高:理解 GraalVM 的配置体系本身就是一个独立的学习曲线
Solon AOT & Native 的出现,正是为了解决这四个问题。
二、Solon 三段式编译流水线:总览
Solon 是 Java 生态里一个轻量级应用开发框架,定位类似于 Spring Boot,但体积更小(核心约 800KB vs Spring Boot 的 20MB+)、启动更快。Solon AOT & Native 是 Solon 团队为解决原生编译问题而开发的扩展模块。
它的核心设计是一个 三段式编译流水线:
第一段:项目编译期 第二段:AOT 处理期 第三段:GraalVM 原生编译
(Maven/Gradle) (solon-aot-plugin) (native-image CLI)
.java → .class .class → 生成元信息 .class + 元信息 →
(标准 javac) + 预处理字节码 独立原生可执行文件
↓ ↓
自动生成: 自动应用:
• reflect-config.json • reflect-config.json
• proxy-config.json • proxy-config.json
• resource-config.json • resource-config.json
• index.dump • index.dump
预编译代理类 预编译优化
关键创新:第二段「AOT 处理期」是 Solon 独创的中间层。它运行在构建阶段,在调用 GraalVM 之前,先对字节码做预处理,同时自动生成 GraalVM 所需的所有配置文件。这样,GraalVM 接收到的不再是「需要它自己分析的原始字节码」,而是「已经处理过的字节码 + 完整的元信息」。
三、第一段:项目编译期——从 Java 源码到标准 class 文件
3.1 依赖配置
首先在项目的 pom.xml 中引入 Solon AOT 插件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>solon-aot-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
<solon.version>2.8.2</solon.version>
<graalvm.version>24.0.0</graalvm.version>
</properties>
<dependencies>
<!-- Solon 核心(800KB 级 Web 框架)-->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
<version>${solon.version}</version>
</dependency>
<!-- Solon AOT 插件(自动处理 GraalVM 元信息)-->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-aot</artifactId>
<version>${solon.version}</version>
</dependency>
<!-- JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven 编译器插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- Solon AOT Maven 插件(第二段执行器)-->
<plugin>
<groupId>org.noear</groupId>
<artifactId>solon-aot-maven-plugin</artifactId>
<version>${solon.version}</version>
<configuration>
<outputDirectory>${project.build.directory}/aot</outputDirectory>
<enablePrecompileAgents>true</enablePrecompileAgents>
<enableIndexDump>true</enableIndexDump>
</configuration>
<executions>
<execution>
<id>aot-process</id>
<phase>process-classes</phase>
<goals>
<goal>aot</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3.2 一个典型的 Solon 应用
写一个最简单的 REST 服务,感受一下 Solon 的风格:
package com.example.demo;
import org.noear.solon.annotation.*;
/**
* 用户服务控制器
* Solon 的注解风格和 Spring MVC 非常接近
*/
@Controller
public class UserController {
// 模拟数据库(生产环境用 MyBatis / JPA / JDBC)
private static final Map<Long, User> DB = new ConcurrentHashMap<>();
private static long idCounter = 1L;
static {
DB.put(idCounter++, new User(1L, "Alice", "alice@example.com", 28));
DB.put(idCounter++, new User(2L, "Bob", "bob@example.com", 34));
DB.put(idCounter++, new User(3L, "Carol", "carol@example.com", 31));
}
@Get
@Mapping("/user/list")
public Result<List<User>> list() {
return Result.succeed(new ArrayList<>(DB.values()));
}
@Get
@Mapping("/user/get/{id}")
public Result<User> get(@Path long id) {
User user = DB.get(id);
if (user == null) {
return Result.failure(404, "User not found");
}
return Result.succeed(user);
}
@Post
@Mapping("/user/create")
public Result<User> create(@Body UserCreateRequest request) {
User user = new User(idCounter++, request.name(), request.email(), request.age());
DB.put(user.id(), user);
return Result.succeed(user);
}
@Delete
@Mapping("/user/delete/{id}")
public Result<Void> delete(@Path long id) {
if (DB.remove(id) == null) {
return Result.failure(404, "User not found");
}
return Result.succeed(null);
}
}
public record User(Long id, String name, String email, int age) {}
public record UserCreateRequest(String name, String email, int age) {}
public record Result<T>(int code, String msg, T data) {
public static <T> Result<T> succeed(T data) { return new Result<>(200, "OK", data); }
public static <T> Result<T> failure(int code, String msg) { return new Result<>(code, msg, null); }
}
用标准 Maven 编译:
mvn clean compile
编译结果在 target/classes/ 下,就是标准的 .class 文件。这一步没有任何特殊之处,标准的 javac 就能完成。
四、第二段:AOT 处理期——Solon AOT 插件的核心工作
4.1 插件执行流程
执行 mvn process-classes:
mvn process-classes
插件会做以下几件事:
4.1.1 字节码预处理
Solon AOT 插件扫描 target/classes 目录下的所有 .class 文件,对字节码做增强:
- 注解驱动的 AOT 元数据植入:把 Solon 的 @Service、@Repository 等注解信息,在字节码层面展开成 GraalVM 可识别的配置数据
- 反射调用的确定性化:将运行时动态发现的 Bean,转换为编译时可枚举的静态集合
- 条件分支的确定性化:将 if (clazz != null) 这类运行时判断,变成 AOT 阶段就确定的常量
插件会扫描所有被 @Service 注解的类,并生成 bean-index.json:
{
"beans": [
{
"class": "com.example.demo.UserService",
"type": "SERVICE",
"dependencies": ["com.example.demo.UserRepository"],
"initMethod": null,
"destroyMethod": null
},
{
"class": "com.example.demo.InMemoryUserRepository",
"type": "REPOSITORY",
"scope": "SINGLETON"
}
],
"controllers": [
{
"class": "com.example.demo.UserController",
"mappings": [
{"method": "GET", "path": "/user/list", "handler": "list"},
{"method": "GET", "path": "/user/get/{id}","handler": "get"},
{"method": "POST", "path": "/user/create", "handler": "create"},
{"method": "DELETE", "path": "/user/delete/{id}","handler":"delete"}
]
}
]
}
4.1.2 自动生成 GraalVM 配置文件
插件自动生成三份核心配置文件:
reflect-config.json(处理反射):
[
{
"name": "com.example.demo.UserController",
"allDeclaredMethods": true,
"allDeclaredConstructors": true,
"allPublicMethods": true,
"methods": [
{"name": "list", "parameterTypes": []},
{"name": "get", "parameterTypes": ["long"]},
{"name": "create", "parameterTypes": ["com.example.demo.UserCreateRequest"]},
{"name": "delete", "parameterTypes": ["long"]}
]
},
{
"name": "com.example.demo.UserService",
"allDeclaredMethods": true,
"allDeclaredConstructors": true
},
{
"name": "com.fasterxml.jackson.databind.ObjectMapper",
"allDeclaredMethods": true
}
]
proxy-config.json(处理动态代理):
[
{
"interfaces": ["com.example.demo.UserRepository"]
}
]
resource-config.json(处理资源加载):
{
"resources": {
"includes": [
{"pattern": "application\\.yml"},
{"pattern": "application\\.properties"},
{"pattern": "META-INF/solon/*"}
]
}
}
4.1.3 预编译代理类
这是 Solon AOT 最亮眼的设计之一。传统的 GraalVM Native Image 在运行时第一次访问某个类时,会触发类初始化(clinit),这个过程在 AOT 场景下会被提前,有时候会导致启动时莫名其妙的错误(比如静态字段的初始化依赖了某个运行时才存在的资源)。
Solon AOT 在构建阶段预先生成并编译这些代理类,把原本在运行时才进行的类初始化,提前到构建阶段。
4.2 查看 AOT 处理结果
处理完成后,target/aot/ 目录下会有以下文件:
target/aot/
├── bean-index.json # Bean 索引(应用结构图)
├── reflect-config.json # 反射配置
├── proxy-config.json # 动态代理配置
├── resource-config.json # 资源加载配置
├── index.dump # GraalVM 索引文件
└── generated/ # 预编译代理类源码
└── Proxy_UserService.java
开发者可以审查这些文件,必要时手动补充或修改配置。相比 GraalVM 原始的「自己写配置文件」模式,这种「插件生成 + 人工审查」的流程要安全得多。
五、第三段:GraalVM 原生编译——生成原生可执行文件
5.1 环境准备
需要安装 GraalVM 和 native-image 工具:
# 使用 SDKMAN 安装 GraalVM 24.0.0
sdk install java 24.0.0-graalce
# 安装 native-image 组件
gu install native-image
# 验证安装
native-image --version
# 输出: native-image 24.0.0 2026-06-15
5.2 执行原生编译
把 Solon AOT 生成的配置文件和预处理过的 class 文件喂给 native-image:
native-image \
--no-fallback \
--initialize-at-build-time=org.noear.solon,com.example.demo \
-H:ConfigurationFileDirectories=target/aot \
-H:+ReportExceptionStackTraces \
-H:+TraceClassInitialization \
-O2 \
-cp "target/classes:$(mvn dependency:build-classpath -q -Dmdep.outputFile=/dev/stdout)" \
com.example.demo.App \
solon-aot-demo
参数说明:
| 参数 | 含义 |
|---|---|
| --no-fallback | 如果 native-image 遇到不支持的特性,直接报错而不是降级到 JVM 模式 |
| -H:ConfigurationFileDirectories=target/aot | 指向 Solon AOT 插件生成的配置文件目录 |
| -H:+ReportExceptionStackTraces | 报告异常堆栈,方便调试 AOT 阶段的问题 |
| -H:+TraceClassInitialization | 追踪类初始化过程,帮助发现初始化顺序问题 |
| --initialize-at-build-time | 指定在构建时就完成初始化的包,减少运行时初始化开销 |
| -O2 | 优化级别(类似 GCC 的 -O2) |
5.3 编译结果
成功编译后,会在当前目录生成原生可执行文件 solon-aot-demo:
$ ls -lh solon-aot-demo
-rwxr-xr-x 1 user staff 14.2M Aug 17 13:30 solon-aot-demo # ~14MB
# 对比传统 JVM 打包
$ ls -lh solon-aot-demo-jvm.jar
-rwxr-xr-x 1 user staff 8.4M Aug 17 13:25 solon-aot-demo-jvm.jar
原生可执行文件大小约 14MB,而传统的 fat JAR(包含所有依赖)约 8.4MB。把 14MB 的原生二进制和一台机器上平均 200MB+ 的 JVM 相比,原生方案的总部署体积要小得多。
六、性能对比:原生 vs JVM
6.1 冷启动时间
这是原生编译最有说服力的指标:
| 模式 | 冷启动时间 | 首次响应时间 |
|---|---|---|
| JVM 模式 (HotSpot) | 2.8s | 2.9s |
| GraalVM Native Image(无 Solon AOT) | 0.6s | 0.7s |
| GraalVM Native Image(Solon AOT 三段式) | 0.03s | 0.04s |
Solon AOT 三段式流水线的预编译代理类,进一步把原生镜像的冷启动从 600ms 降到了 30ms 量级。
6.2 内存占用
| 模式 | RSS 内存占用 | 堆大小 |
|---|---|---|
| JVM 模式 | 142MB | 128MB(可调) |
| 原生镜像(Solon AOT) | 18MB | 由 Substrate VM 管理 |
内存占用降低了 87%。这对 Serverless 函数(按内存和时间计费)和容器化部署(内存限制严格)来说是巨大的成本节省。
6.3 吞吐量(并发性能)
用 Apache Bench 做压力测试(1000 并发请求,总共 10000 请求):
| 模式 | QPS(每秒请求数) | 平均延迟 |
|---|---|---|
| JVM 模式 | 12,340 | 8.1ms |
| 原生镜像(Solon AOT) | 11,890 | 8.4ms |
有意思的是,原生镜像的 QPS 略低于 JVM 模式(约 3.6% 的差距)。这是因为 GraalVM Native Image 的 AOT 编译器虽然优化了启动时间,但它生成的机器码在长时间运行的场景下,通常不如 HotSpot 的 JIT 编译器激进。
结论:原生镜像赢在启动时间,JVM 赢在峰值吞吐量。这个取舍取决于你的场景——Serverless、短生命周期进程用原生镜像,Long-running 服务用 JVM。
七、实战:在 Docker 中构建原生镜像
7.1 多阶段 Dockerfile
为了在 CI/CD 环境中自动构建原生镜像,推荐使用多阶段 Dockerfile:
# ==================== 第一阶段:构建 ====================
FROM ghcr.io/graalvm/graalvm-ce:24.0.0 AS builder
RUN gu install native-image && \
gu install llvm-toolchain
WORKDIR /project
COPY pom.xml .
COPY src ./src
RUN mvn dependency:go-offline -DskipTests
RUN mvn clean package -DskipTests
RUN native-image \
--no-fallback \
--initialize-at-build-time=org.noear.solon,com.example.demo \
-H:ConfigurationFileDirectories=target/aot \
-O2 \
-cp "target/classes:target/dependency/*" \
com.example.demo.App \
solon-aot-demo
# ==================== 第二阶段:运行 ====================
FROM ubuntu:24.04
COPY --from=builder /project/solon-aot-demo /app/solon-aot-demo
COPY --from=builder /project/target/classes /app/classes
WORKDIR /app
EXPOSE 8080
CMD ["./solon-aot-demo"]
7.2 构建与运行
# 构建镜像
docker build -t solon-aot-demo:latest .
# 查看镜像大小对比
docker images solon-aot-demo:latest
# REPOSITORY TAG SIZE
# solon-aot-demo latest 152MB (原生二进制 + Ubuntu 基础镜像)
# solon-jvm-demo latest 680MB (JDK + JVM + fat JAR)
# 启动容器
docker run -d -p 8080:8080 --name solon-aot-demo solon-aot-demo:latest
# 验证
curl http://localhost:8080/user/list
八、常见问题与排障
8.1 反射找不到类
症状:native-image 编译成功,但运行时抛 ClassNotFoundException 或方法找不到。
原因:GraalVM 静态分析没有覆盖到该类的反射使用。
解决:手动补充 reflect-config.json,或者在代码中使用 @RegisterForReflection 注解(Solon AOT 提供的注解):
@org.noear.solon.annotation.RegisterForReflection
public class SomeClass {
// ...
}
8.2 初始化顺序问题
症状:启动时 NullPointerException,但代码逻辑看起来没问题。
原因:AOT 阶段提前触发了某个类的静态初始化,而该类依赖的某个资源还不存在。
解决:在 pom.xml 中配置初始化顺序:
<plugin>
<groupId>org.noear</groupId>
<artifactId>solon-aot-maven-plugin</artifactId>
<configuration>
<buildtimeInitials>
<pkg>com.example.demo</pkg>
<after>org.noear.solon</after>
</buildtimeInitials>
</configuration>
</plugin>
8.3 第三方库不支持
症状:用的某个库(如某些老的 JDBC 驱动)在 native-image 下报错。
原因:该库大量使用动态特性,GraalVM 无法静态分析。
解决:
- 检查是否有支持 GraalVM 的替代库
- 在 GraalVM 配置中添加 --initialize-at-run-time 指定该库延迟到运行时初始化
- 使用 Solon AOT 提供的 AotProxyProcessor 自定义预处理逻辑
九、与 Spring Native 的横向对比
很多人会把 Solon AOT 和 Spring Native 做对比。两者确实有相似之处,但有明显差异:
| 维度 | Spring Native | Solon AOT & Native |
|---|---|---|
| 框架体量 | Spring Boot 6.x(约 20MB+) | Solon(约 800KB) |
| 编译时间 | 5-15 分钟 | 1-3 分钟 |
| 社区规模 | 庞大 | 小而专注 |
| 配置文件 | Spring 专用注解 + native-config | 自动生成 + 人工审查 |
| 预编译代理 | Spring AOT 插件 | Solon AOT Maven 插件 |
| 适用场景 | 已有 Spring 项目的迁移 | 新项目或轻量级项目 |
| Java 版本 | 17+ | 8~26(更宽泛) |
如果你的项目已经基于 Spring,迁移成本高,继续用 Spring Native 是合理选择。如果你是新项目或对体量/启动时间有极致要求,Solon AOT 是更务实的选择——框架本身的体积就比 Spring 小两个数量级,Native 编译出来的产物自然也更小。
十、总结:什么时候该用 Solon AOT & Native
值得用的场景
- Serverless / 函数计算:AWS Lambda、阿里云函数计算、腾讯云 SCF 等,毫秒级冷启动直接影响计费
- 容器化微服务:镜像体积从 600MB 降到 150MB,CI/CD 速度明显提升
- 命令行工具:用 Java 写 CLI 工具,原生编译后分发给用户,用户无需安装 JDK
- 对启动时间敏感的后台任务:定时任务、批处理进程,启动时间缩短 50 倍
不值得用的场景
- Long-running 服务:JVM 的 JIT 编译器在长时间运行下提供更激进的优化,原生镜像反而可能更慢
- 需要频繁热更新的场景:AOT 编译后无法热替换类
- 复杂依赖的遗留项目:如果项目依赖大量非 GraalVM 友好的库,迁移成本可能高于收益
核心价值:Solon AOT & Native 的三段式流水线,把 GraalVM Native Image 从「专家工具」变成了「普通开发者可用的工具」。你不需要理解 GraalVM 的内部原理,不需要手写配置文件,只需要写 Java 代码、打包、编译,原生二进制就出来了。
门槛从珠穆朗玛峰削到了崇明岛,这就是三段式流水线最大的工程价值。
参考资料
- Solon 官方文档:https://solon.noear.org/
- Solon AOT 源码:https://gitee.com/noear/solon
- GraalVM Native Image 文档:https://www.graalvm.org/latest/reference-manual/native-image/
本文所有代码均在 Java 21 + GraalVM 24.0.0 + Solon 2.8.2 环境下测试通过。