GraalVM 原生编译
GraalVM Native Image 将 Spring Boot 应用编译成原生可执行文件:无需 JVM 启动,冷启动通常在 100 ms 以内,内存占用降低 60~80%。Spring Boot 3.x 通过 AOT(Ahead-Of-Time)处理 内置对 GraalVM 的官方支持。
原理概述
传统 JVM 方式
源码 → 字节码(.class)→ JVM 解释/JIT → 运行
GraalVM Native Image 方式
源码 → 字节码 → GraalVM(静态分析 + AOT 编译)→ 原生可执行文件 → 直接运行
Spring AOT 处理阶段(构建时):
- 分析所有
@Configuration、@Bean、条件注解 - 生成确定性的 Bean 定义代码(替代运行时反射扫描)
- 为反射、代理、序列化、资源访问生成 hints(配置文件)
- GraalVM 静态分析上述 hints,将可达代码一起编译进二进制
环境准备
安装 GraalVM JDK
# 推荐通过 SDKMAN 安装
sdk install java 21.0.3-graalce
sdk use java 21.0.3-graalce
# 验证
java -version
# => java version "21.0.3" ... GraalVM CE ...
native-image --version
# => GraalVM Runtime Environment GraalVM CE 21.0.3+7.1GraalVM 21+ 已内置
native-image工具,无需单独安装native-image组件。
项目配置
pom.xml(Maven)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>build.gradle(Gradle)
plugins {
id 'org.springframework.boot' version '3.3.0'
id 'org.graalvm.buildtools.native' version '0.10.2'
}
graalvmNative {
binaries {
main {
imageName = 'myapp'
buildArgs.add('--verbose')
}
}
}编译与运行
# Maven:编译原生可执行文件(耗时 2~10 分钟)
./mvnw -Pnative native:compile
# 运行(不依赖 JVM)
./target/myapp
# 启动日志:Started Application in 0.082 seconds (process running for 0.089)
# Gradle
./gradlew nativeCompile
./build/native/nativeCompile/myapp使用 Docker 构建(无需本地 GraalVM)
# Maven:使用 Buildpack 在容器内编译
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp-native \
-Pnative
# 运行原生镜像
docker run --rm -p 8080:8080 myapp-nativeDockerfile 多阶段原生编译:
# ── 阶段一:AOT 编译 ───────────────────────────────────────
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /build
# 安装 Maven(或使用已包含 Maven 的基础镜像)
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -q
COPY src ./src
RUN ./mvnw native:compile -Pnative -DskipTests
# ── 阶段二:最小运行镜像 ───────────────────────────────────
FROM debian:bookworm-slim
RUN useradd -r -s /bin/false appuser
USER appuser
COPY --from=builder /build/target/myapp /app/myapp
EXPOSE 8080
ENTRYPOINT ["/app/myapp"]最终镜像无 JVM,体积通常在 60~120 MB(相比传统镜像 200~400 MB)。
处理反射与动态特性
AOT Hints
GraalVM 静态分析无法发现运行时通过反射/代理加载的类,需要提供 hints:
@Configuration
@ImportRuntimeHints(MyAppRuntimeHints.class)
public class AppConfig { }
public class MyAppRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// 注册需要反射访问的类
hints.reflection()
.registerType(MyDto.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.DECLARED_FIELDS)
.registerType(ThirdPartyClass.class,
MemberCategory.INVOKE_PUBLIC_METHODS);
// 注册资源文件
hints.resources()
.registerPattern("templates/*.html")
.registerPattern("i18n/*.properties");
// 注册 JDK 动态代理接口
hints.proxies()
.registerJdkProxy(MyService.class);
// 注册序列化类型
hints.serialization()
.registerType(MySerializable.class);
}
}@RegisterReflectionForBinding
用于 JSON 序列化/反序列化的 DTO:
@RestController
@RegisterReflectionForBinding(OrderDto.class)
public class OrderController {
@PostMapping("/orders")
public OrderDto createOrder(@RequestBody OrderDto dto) {
return dto;
}
}@Reflective
直接标注在需要反射访问的类上:
@Component
@Reflective // 允许反射访问该 Bean
public class DynamicProcessor {
public void process(String type) {
// 内部可能用到反射
}
}常见兼容性问题
| 特性 | 原生编译支持 | 说明 |
|---|---|---|
| 反射 | 需要 hints | 运行时动态反射须显式声明 |
| JDK 动态代理 | 需要 hints | 接口代理须注册 |
| CGLIB 代理 | Spring 3.x 用 AOT 代理替代 | @Configuration(proxyBeanMethods=false) 无代理 |
| 类路径扫描 | AOT 阶段完成,运行时不扫描 | 不能在运行时动态添加类 |
| 反序列化 | 需要 hints | Jackson、Gson 均需配置 |
Class.forName() | 静态已知类可用 | 动态类名字符串需要 hints |
| Groovy / 动态语言 | 不支持 |
Jackson 配置
@Configuration
public class JacksonNativeConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> builder
.featuresToDisable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
// native image 下禁用懒加载反序列化
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
}测试原生可执行文件
// 使用 @NativeImageTest 在真实原生镜像中运行测试(Gradle)
// Maven 使用 native:test goal
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = "spring.profiles.active=test")
class OrderControllerNativeTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void createOrder() {
OrderDto dto = new OrderDto("item-1", 2);
ResponseEntity<OrderDto> response =
restTemplate.postForEntity("/orders", dto, OrderDto.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}# Maven:在原生镜像中运行测试(构建原生测试二进制)
./mvnw -Pnative test
# 提前用 Agent 收集 hints(运行普通 JVM 测试时自动生成 hints 配置文件)
./mvnw -Pagent testAOT 阶段调试
# 查看 AOT 生成的源码(Maven)
./mvnw spring-boot:process-aot
# 生成的代码位于
ls target/spring-aot/main/sources/
# 查看 native-image 分析报告
./mvnw -Pnative native:compile -Dnative.buildArgs="--report-unsupported-elements-at-runtime"性能对比
| 指标 | JVM(JIT) | GraalVM Native |
|---|---|---|
| 冷启动时间 | 2~10 s | 50~200 ms |
| 内存占用(空载) | 200~500 MB | 30~100 MB |
| 峰值吞吐量 | 高(JIT 充分优化后) | 略低(无 JIT) |
| 编译时间 | 秒级 | 分钟级 |
| 镜像体积 | 200~400 MB | 60~120 MB |
对于短生命周期(Serverless、CLI 工具)或内存敏感场景,原生编译优势显著;对于长期运行、高并发服务,JVM + JIT 峰值吞吐更高。