java -jar跑得好好的,换成 native image,启动是快了,构建先给你来一闷棍。
我这次拿的是 SpringBoot 3.2、JDK21、GraalVM 这一套。版本不用搞得太玄乎,SpringBoot 3.2 本身要求 Java 17 起步,并兼容 Java 21;GraalVM Native Image 走的是 AOT,把应用提前编译成独立可执行文件,启动和内存占用一般会比 JVM 模式轻不少。这个方向 Spring 官方也已经放进正常支持里,不再是以前那种边角料玩法。
我先起了一个很薄的接口,不上数据库,不接 MQ。第一次上手别一口气把公司祖传项目塞进去,里面反射、动态代理、XML、老 SDK 一坨东西,报错会把人看烦。
pom.xml里关键配置大概这样:
<java.version>21</java.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
</plugins>
业务代码我写得很小,就一个查订单状态的接口:
@RestController
@RequestMapping("/internal/orders")
class OrderProbeController {
private final OrderSnapshotService snapshotService;
OrderProbeController(OrderSnapshotService snapshotService) {
this.snapshotService = snapshotService;
}
@GetMapping("/{orderNo}/snapshot")
OrderSnapshot snapshot(@PathVariable String orderNo) {
if (orderNo == null || orderNo.length() < 8) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "orderNo not right");
}
return snapshotService.query(orderNo);
}
}
@Service
class OrderSnapshotService {
OrderSnapshot query(String orderNo) {
var status = orderNo.endsWith("9") ? "WAIT_PAY" : "PAID";
return new OrderSnapshot(orderNo, status, LocalDateTime.now());
}
}
record OrderSnapshot(String orderNo, String status, LocalDateTime checkedAt) {
}
JVM 模式跑一下:
mvn clean package
java -jar target/graal-demo-0.0.1-SNAPSHOT.jar
没问题,再打 native:
mvn -Pnative native:compile
./target/graal-demo
这里第一感觉很明显:启动确实利索,尤其是这种小服务,进程起来到端口可用,中间没多少拖泥带水。这个东西适合什么?我第一反应不是大型后台管理系统,而是网关边缘服务、定时任务、小接口、Serverless、K8s 里频繁扩缩容的服务。
但 native image 也不是白送的。
编译阶段会明显慢,而且吃机器。你别拿它跟普通mvn package比,那不是一个动作。普通 jar 更像把东西装箱,native 更像提前把一堆运行期要干的活都算掉。Spring Boot 文档里也明确说,native image 是通过 ahead-of-time 处理生成独立可执行文件。
我这里最先踩到的是反射。
比如有些老代码喜欢这么干:
class LegacyMapper {
Object fill(String className, Map<String, Object> row) {
try {
Class<?> type = Class.forName(className);
Object target = type.getDeclaredConstructor().newInstance();
for (var field : type.getDeclaredFields()) {
Object value = row.get(field.getName());
if (value == null) {
continue;
}
field.setAccessible(true);
field.set(target, value);
}
return target;
} catch (Exception e) {
throw new IllegalStateException("map row failed, class=" + className, e);
}
}
}
这段在 JVM 里很常见,虽然我也不太喜欢。到了 native image 里,问题就来了:运行期动态找类、动态构造、动态改字段,GraalVM 不一定知道你要用它。
修法不是到处加 try-catch。要么把这种写法砍掉,改成明确代码;要么老老实实给 hint。
比如这个对象确实要反射构造:
public class ExportLine {
private String orderNo;
private String status;
public ExportLine() {
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public void setStatus(String status) {
this.status = status;
}
}
补一个 RuntimeHints:
@Configuration
class NativeRuntimeConfig implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection()
.registerType(ExportLine.class, builder -> builder
.withConstructor(Collections.emptyList(), ExecutableMode.INVOKE)
.withField("orderNo")
.withField("status")
.withMethod("setOrderNo", List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE)
.withMethod("setStatus", List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE));
}
}
然后在启动类上挂一下:
@SpringBootApplication
@ImportRuntimeHints(NativeRuntimeConfig.class)
public class GraalDemoApplication {
public static void main(String[] args) {
SpringApplication.run(GraalDemoApplication.class, args);
}
}
这地方我建议别一上来就迷信“框架都会处理”。Spring 自己那套常规 Bean、Controller、配置类,大多数已经处理得不错;真正麻烦的,往往是项目里那些工具类、老 SDK、自己封装的动态加载逻辑。
还有一个体验点是 JDK21 的虚拟线程。
SpringBoot 3.2 这波跟 Java 21 放一起看,确实舒服。Spring 官方当时也专门提到 Spring Boot 3.2、GraalVM native image、Java 21 virtual threads 这一组配合。
配置很简单:
spring:
threads:
virtual:
enabled: true
但我不会因为它简单就到处开。IO 型接口可以试,尤其是大量外部 HTTP 调用、等待数据库返回这类场景。CPU 打满的活,开虚拟线程也救不了,该排队还是排队,该限流还是限流。
我一般会加一个小接口看线程名,别靠猜:
@RestController
class ThreadCheckController {
@GetMapping("/debug/thread")
Map<String, String> thread() {
Thread t = Thread.currentThread();
return Map.of(
"name", t.getName(),
"virtual", String.valueOf(t.isVirtual())
);
}
}
如果线上要试 native,我会按这个顺序来,不会直接替换主链路。
先挑一个边缘服务,依赖少一点,最好只有 HTTP 和少量 JSON。
再跑 JVM 模式的集成测试。
再打 native,补 runtime hints。
最后压启动、内存、接口耗时,不要只看启动快。native image 的长稳态性能,不一定每个场景都赢 JVM。JIT 跑热之后的优化能力还在,别把 native 当银弹。
我这次上手后的感觉很直接:
小服务,很香。
构建,挺慢。
老项目迁移,别急。
反射多、动态代理多、运行期加载多的项目,先把这些地方翻出来。否则你会看到一种很烦的情况:编译能过,启动能过,某个隐藏分支一跑才炸。
GraalVM 不是让 Java 变成 Go,也不是把 Spring 变轻的魔法。它更像是把运行期的不确定性提前摊牌。摊得出来,启动和内存就好看;摊不出来,就得你自己补账。
SpringBoot3.2 + JDK21 + GraalVM 这套可以上手,而且值得上手。
但我会先拿它处理“小、冷、弹、短”的服务。核心大单体先别冲动,里面埋的反射和老包,够你喝一壶。