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 处理阶段(构建时):

  1. 分析所有 @Configuration@Bean、条件注解
  2. 生成确定性的 Bean 定义代码(替代运行时反射扫描)
  3. 为反射、代理、序列化、资源访问生成 hints(配置文件)
  4. 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.1

GraalVM 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-native

Dockerfile 多阶段原生编译:

# ── 阶段一: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 阶段完成,运行时不扫描不能在运行时动态添加类
反序列化需要 hintsJackson、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 test

AOT 阶段调试

# 查看 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 s50~200 ms
内存占用(空载)200~500 MB30~100 MB
峰值吞吐量高(JIT 充分优化后)略低(无 JIT)
编译时间秒级分钟级
镜像体积200~400 MB60~120 MB

对于短生命周期(Serverless、CLI 工具)或内存敏感场景,原生编译优势显著;对于长期运行、高并发服务,JVM + JIT 峰值吞吐更高。


相关链接