编程 Solon AOT & Native:三段式编译,把 Java 原生编译的门槛从珠穆朗玛峰削到崇明岛

2026-08-17 13:15:31 +0800 CST views 5

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。

但这套流程有几个问题:

  1. 覆盖率难保证:录制时跑的路径不一定覆盖了所有运行场景,漏掉的路径在生产环境就炸了
  2. 版本漂移:依赖升级后,之前录制的配置文件可能不再适用
  3. 调试困难:出了问题是 AOT 分析的锅、还是配置的锅、还是代码本身的锅,很难定位
  4. 门槛高:理解 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.8s2.9s
GraalVM Native Image(无 Solon AOT)0.6s0.7s
GraalVM Native Image(Solon AOT 三段式)0.03s0.04s

Solon AOT 三段式流水线的预编译代理类,进一步把原生镜像的冷启动从 600ms 降到了 30ms 量级。

6.2 内存占用

模式RSS 内存占用堆大小
JVM 模式142MB128MB(可调)
原生镜像(Solon AOT)18MB由 Substrate VM 管理

内存占用降低了 87%。这对 Serverless 函数(按内存和时间计费)和容器化部署(内存限制严格)来说是巨大的成本节省。

6.3 吞吐量(并发性能)

用 Apache Bench 做压力测试(1000 并发请求,总共 10000 请求):

模式QPS(每秒请求数)平均延迟
JVM 模式12,3408.1ms
原生镜像(Solon AOT)11,8908.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 无法静态分析。

解决

  1. 检查是否有支持 GraalVM 的替代库
  2. 在 GraalVM 配置中添加 --initialize-at-run-time 指定该库延迟到运行时初始化
  3. 使用 Solon AOT 提供的 AotProxyProcessor 自定义预处理逻辑

九、与 Spring Native 的横向对比

很多人会把 Solon AOT 和 Spring Native 做对比。两者确实有相似之处,但有明显差异:

维度Spring NativeSolon 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

值得用的场景

  1. Serverless / 函数计算:AWS Lambda、阿里云函数计算、腾讯云 SCF 等,毫秒级冷启动直接影响计费
  2. 容器化微服务:镜像体积从 600MB 降到 150MB,CI/CD 速度明显提升
  3. 命令行工具:用 Java 写 CLI 工具,原生编译后分发给用户,用户无需安装 JDK
  4. 对启动时间敏感的后台任务:定时任务、批处理进程,启动时间缩短 50 倍

不值得用的场景

  1. Long-running 服务:JVM 的 JIT 编译器在长时间运行下提供更激进的优化,原生镜像反而可能更慢
  2. 需要频繁热更新的场景:AOT 编译后无法热替换类
  3. 复杂依赖的遗留项目:如果项目依赖大量非 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 环境下测试通过。

推荐文章

api远程把word文件转换为pdf
2024-11-19 03:48:33 +0800 CST
MySQL用命令行复制表的方法
2024-11-17 05:03:46 +0800 CST
JavaScript 策略模式
2024-11-19 07:34:29 +0800 CST
HTML + CSS 实现微信钱包界面
2024-11-18 14:59:25 +0800 CST
WebSocket在消息推送中的应用代码
2024-11-18 21:46:05 +0800 CST
SpaceX 600亿美元收购Cursor(中篇)
2026-06-22 03:30:23 +0800 CST
程序员茄子在线接单