首页
学习
活动
专区
圈层
工具
发布

SpringBoot3.2 + jdk21 + GraalVM上手体验

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 这套可以上手,而且值得上手。

但我会先拿它处理“小、冷、弹、短”的服务。核心大单体先别冲动,里面埋的反射和老包,够你喝一壶。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OpKlK0S9aTTlWukevivkeRwg0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券