# 使用GraalVM Native构建Springboot微服务后记

- URL: https://blog.xiaoxian.org/articles/%E4%BD%BF%E7%94%A8graalvm-native%E6%9E%84%E5%BB%BAspringboot%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%90%8E%E8%AE%B0
- Author: Darf
- Published: 2026-01-21
- Tags: Java, Springboot, Native, GraalVM

> 把一个 Spring Boot 多模块后端（AxT Dashboard Backend）尝试用 GraalVM Native Image / AOT 编译成原生可执行文件

---

# 尝试了一下 Java 的 GraalVM Native / AOT：确实更省内存、更快启动，但现阶段不适合我

> 把一个 Spring Boot 多模块后端（AxT Dashboard Backend）尝试用 **GraalVM Native Image / AOT** 编译成原生可执行文件。
>
> 结论先放在前面：
> - 运行层面很香：**内存占用能做到 20-40MB**，启动也明显更快。
> - 但构建层面很劝退：**单模块 Native 构建至少 10 分钟**，并且对机器要求很高（我这次测试环境 **8C / 8G**，**4G 会 OOM**）。
>
> 所以目前我的策略是：**方案跑通，形成脚本与配置，先归档**。等未来真要全量转 Native，再把这套东西拿出来继续推进。
>
> 在此之前我也查询过很多资料，比如这篇Reddit讨论: [GraalVM 是首选吗](https://www.reddit.com/r/java/comments/1gil2lb/is_graalvm_the_goto_choice/?tl=zh-hans)

---

## 1. 背景：我为什么要折腾 Native Image

我最开始的动机其实很朴素：
- [GraalVM Native Image 与传统 JVM 内存管理:云原生时代的技术选型指南](https://juejin.cn/post/7576489970761105414)
- **Spring Boot 服务跑起来后，JVM 进程常驻内存不低**（尤其一堆模块拆开跑）。
- 我的 6个微服务模块，实测 docker 上机后单个容器 **430MB+**，刚启动3G内存没了
- 为了节省成本，目前生产机器 **4C4G**，可能还需要蓝绿部署，双倍内存成本暴增
- 看看 Native Image 能不能让后端服务在测试/轻量环境里“更轻”。
- 高并发场景下内存的使用量甚至可以比肩Go语言
> 参考 [2024年各编程语言运行100万个并发任务需要多少内存？](https://cloud.tencent.com/developer/article/2483970)

GraalVM Native Image 的卖点也很明确：
- **更快启动**（省掉 JIT 预热）
- **更低内存**（很多场景能显著下降）

但它的代价同样出名：
- **构建慢**
- **兼容性坑多**（反射、动态代理、资源加载、Spring Cloud 特性……）

我这次做的不是“看文档跑 demo”，而是直接把它落到真实项目上：
- 后端是 Maven 多模块
- Spring Boot 3.5.x + Java 17
- 用了 Spring Cloud Config（配置中心）

---

## 2. 在选 Native 之前，我做过什么

在决定上 Native 之前，我其实是按“正常工程思路”先评估/尝试了一圈：

- **先接受 JVM 的现实**：服务多模块拆开跑，单个服务不大，但整体常驻内存并不低。
- **先把可观测性拉起来**：关注的核心指标很明确：
  - 启动时间
  - 空闲内存占用
  - 压测/正常流量下的稳定性
- **再看“跑得更省”的路线**：我需要的是能在测试环境更轻量地跑起来，而不是极致吞吐。
- **尝试通过JVM参数缩小占用**： 使用了 **-Xms，-Xss，-XX:MaxMetaspaceSize** 等一系列限制，但内存占用仍然没有可观的减少，反而会导致启动慢的特性

最终我选择 Native 的原因也很简单：
- 我更在意“**启动更快**、**更低内存**”这两个点。
- 我可以接受“先把路跑通、先归档”的策略，而不是一步到位全量迁移。

---

## 3. 环境与目标

### 3.1 构建环境
- OS: Ubuntu 24.04
- CPU: 8 Core
- RAM: 8 GB（实践证明 4 GB 会 OOM）
- GraalVM: `17.0.9-graalce`
- Maven: 3.9+

### 3.2 目标
我希望做到：
- 在远程编译机上完成单个模块的 Native Image 构建
- 编译成功后直接输出可执行文件路径

---

## 4. 首要问题: AOT 阶段会“启动一部分应用”，Config Server 连接直接炸

Native Image 构建阶段（更准确地说是 AOT processing）会触发 Spring Boot 的一系列初始化。

而我的服务用了 Spring Cloud Config：
- 默认会去连 `http://localhost:28610`

构建机上当然没有 Config Server，于是报错：
- `ConfigClientFailFastException`
- 连接被拒绝

### 5.1 我当时的疑问
“是不是必须完整启动一遍服务才能构建？”

答案是：
- **不需要完整启动**
- 但 AOT 阶段确实会做很多启动期事情，所以像 Config Client 这种“引导期外部依赖”，会在构建时就触发。

### 4.2 解决方案：让 Config Server 变成“可选”

这里我踩到的关键点是：**AOT 期间会触发引导期配置加载**。如果引导期就强依赖外部服务（例如 Config Server），构建就会直接失败。

我的修复做法是两步：

1) 把引导期导入改为可选：
```yml
spring:
  config:
    import: "optional:configserver:"
```

2) 关闭 fail-fast（让“连不上配置中心”变成 warn，而不是直接终止）：
```yml
spring:
  cloud:
    config:
      fail-fast: false
```

这样构建时即使 Config Server 不在线，也只会打印 WARN，不会把 AOT 直接打断。

> - 只改 `fail-fast` 不一定够
> - `optional:configserver:` 才是让引导期导入真正“可选”的开关

---

## 5. 下一波坑：`@RefreshScope` 这类 Spring Cloud 特性与 Native 不友好

Config Server 的连接问题解决后，AOT 继续往下跑，又遇到：
- `AotBeanProcessingException`
- 出错 bean：`OAuthProperties`

我定位到这个配置类上有：
- `@RefreshScope`

`@RefreshScope` 本质是运行时动态刷新 + 代理增强（用于运行时动态刷新配置服务），在 Native Image 下经常会踩到 AOT/代理/反射相关坑。

### 5.1 解决方案

在这次“先把构建跑通”的阶段，我选择了最直接的处理：
- **移除 `@RefreshScope`**（至少先让 AOT 能过，再谈更优雅的方案）

---

## 6. 反射配置：`reflect-config.json` 还是要准备

即使 Spring Boot 3 对 AOT/Native 已经友好很多，但项目里只要有：
- 反射
- Jackson 序列化
- 一些框架自动装配

就很容易遇到运行期找不到构造器/字段之类的问题。

这次为了快速跑通，我把本次验证过的一份反射配置也一起整理归档（方便后续复用/扩展）。

---

## 7. 最终结果：确实能跑，但构建成本太高

### 8.1 结果
- ✅ Native Image 编译成功
- ✅ 可执行文件能跑起来
- ✅ 内存占用明显下降：**20-40MB**
- ✅ 启动速度更快（非常明显，几乎全程加载完10s内，正常JVM则1m左右）

### 8.2 现实问题（也是我选择暂缓的原因）
- ❌ 构建时间太长：**单模块至少 10 分钟**
- ❌ 构建资源消耗太大：
  - **8C/8G** 才比较顺
  - **4G 直接 OOM**（这点对我的测试/CI环境不友好）
  - 构建出的二进制文件比原jar文件大了3倍

因此对我当前阶段来说：
- Native Image 不是不能用
- 但**前期投入产出比不高**，尤其我这里还是多模块（模块越多，构建成本线性甚至更糟）。

---

## 8. 归档：把“跑通所需的一切”统一收口

我最终决定把这套方案“归档而不是删除”，原因很简单：
- 以后如果真的要全量转 Native，这些踩坑经验能直接复用

归档内容主要包括几类：

- **构建脚本**：远程构建、输出产物路径
- **POM 片段**：如何启用 native profile / 插件
- **反射配置示例**：本次验证通过的基础版本
- **关键配置改动清单**：AOT/Native 相关的必要调整点

同时，为了不影响日常生产/开发体验，我也做了回滚/恢复：
- 恢复 `@RefreshScope`
- 恢复 `application.yml` 的 config server 导入方式
- 移除根 POM 的 native profile
- 移除模块 POM 的 native 插件
- 清理模块内的 `META-INF/native-image`（避免长期污染）

---

## 9. 我对“是否要上 Native Image”的个人建议

如果是下面情况，Native Image 会很香：
- 需要极致启动速度（例如 serverless / 弹性扩缩容特别频繁）
- 机器内存特别紧张
- 项目对 Spring Cloud 依赖不重（或者你愿意做适配/替换）

如果是下面情况，建议先观望：
- 多模块 + 依赖复杂 + Spring Cloud 特性多
- CI 资源一般（尤其 RAM < 8G）
- 更在意“构建效率/迭代速度”而不是“运行时极致性能”

我这次的选择就是：
- **现在先不全量上**
- **把路走通并归档**

等未来时机成熟（比如：模块更少、配置中心方案调整、CI 机器更强、业务更需要低内存/快启动），再把它捡起来继续。
