springboot可以把项目打包成app了,Graal打包版

我们都知道,在java的世界,有一个神奇的存在,那就是虚拟机(jvm)。

因为虚拟机的存在,java实现了最早的“一处编译,处处执行”的设计思想,成功在早期的各大语言的竞争中脱颖而出,并在各种后起之秀的冲击下,长期保持最受欢迎的编程语言的头部区域。

实在是太方便了,只要有虚拟机的存在,我们在编程的时候就不用考虑各种运行系统的复杂性与差异,这些由各种系统的虚拟机开发专家们替我们解决了,而我们仅需要关注编码就行了。

但进入了21时间,云原生的兴起,java的最大的优势虚拟机,也渐渐阻碍了其在云上开疆扩土的阻力。

“内存占用高”、“STW” 、 “启动速度慢”等一些虚拟机的固有特性,导致java在go等这些为云而设计的语言面前尽显颓势。

所以,java能不能跳过虚拟机,直接把程序编译打包成可以直接在目标系统上运行的呢?

说实话,刚开始学习java的时候,我就有这个疑问,为了把java的可执行程序(jar)打包成可以在windows上运行的exe程序,当时还搞过很多偏门的的办法。

现在,随着GraalVM的成熟,springboot的也提出了直接将java程序直接打包成系统可执行程序(本地应用)的技术方案。

java本地app部署架构

今天我们就来验证一下!

实现过程

hello world

先写一套hello world的springboot程序,使用maven或gradle打包。

这里和我们正常的项目开发流程完成一致,没有任何不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@SpringBootApplication
public class GraalvmNativeAppApplication {

@RequestMapping("/{name}")
String home(@PathVariable String name) {
String x = "Hello " + name;
System.out.println(x);
return x;
}
public static void main(String[] args) {
SpringApplication.run(GraalvmNativeAppApplication.class, args);
}

}

这是这个项目的业务代码,没有其他的了。

配置GraalVM

pom.xml里配置GraalVM 本地应用打包插件,这是我们的springboot程序能够打包成本地应用的核心入口,一定要有。

1
2
3
4
5
6
7
8
9
10
<build>
<plugins>

<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>

</plugins>
</build>

其他的,项目里就不需要了。

也可以通过start.spring.io来构建项目,spring会把一切配置好,记得看构架好后的HELP.MD文件呦。
springboot初始化时配置graalvm

环境依赖

接下来就是要配置一下系统环境了。

要将项目打包成系统原生运行应用,需要在我们的系统里安装一套本地镜像支持库(Liberica Native Image Kit page),这个nik库实现了将java字节码编译成操作系统二进制执行码这一过程。

安装nik包可以手动安装,也可以使用sdk版本控制工具来安装, 这个比较简单,我使用的是这种方式(我使用的是mac电脑)。

使用sdk安装java nik 的命令如下:

1
sdk install 23.0.7.r17-nik

安装完成后,系统就自动将当前的jdk切换成刚下载的这个jdk了。

1
2
3
4
5
% java --version

openjdk 17.0.14 2025-01-21 LTS
OpenJDK Runtime Environment Liberica-NIK-23.0.7-1 (build 17.0.14+10-LTS)
OpenJDK 64-Bit Server VM Liberica-NIK-23.0.7-1 (build 17.0.14+10-LTS, mixed mode, sharing)

出现这个信息,我们就可以打包了。

打包

命令行输入

1
mvn -Pnative native:compile

这个命令会触发native-maven-pluginmaven插件,执行本地打包app。

这个过程会很慢,而且会很耗cpu,我的电脑cpu基本都跑满了,现在终于理解,那些c程序员们在编译项目时会去抽烟了,确实这段时间啥也干不了。

执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
...
[1/8] Initializing... (3.9s @ 0.14GB)
Java version: 17.0.14+10-LTS, vendor version: Liberica-NIK-23.0.7-1
Graal compiler: optimization level: 2, target machine: armv8-a
C compiler: cc (apple, arm64, 15.0.0)
Garbage collector: Serial GC (max heap size: 80% of RAM)
1 user-specific feature(s)
- org.springframework.aot.nativex.feature.PreComputeFieldFeature
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
[2/8] Performing analysis... [******] (35.5s @ 1.50GB)
16,529 (90.73%) of 18,218 types reachable
27,176 (67.68%) of 40,151 fields reachable
79,715 (63.21%) of 126,116 methods reachable
5,144 types, 357 fields, and 5,957 methods registered for reflection
64 types, 72 fields, and 55 methods registered for JNI access
5 native libraries: -framework CoreServices, -framework Foundation, dl, pthread, z
[3/8] Building universe... (3.9s @ 1.59GB)
[4/8] Parsing methods... [**] (2.7s @ 1.79GB)
[5/8] Inlining methods... [****] (1.6s @ 1.95GB)
[6/8] Compiling methods... [*****] (23.4s @ 1.61GB)
[7/8] Layouting methods... [**] (4.5s @ 1.70GB)
[8/8] Creating image... [***] (7.9s @ 1.29GB)
36.95MB (50.32%) for code area: 51,915 compilation units
35.50MB (48.34%) for image heap: 386,138 objects and 253 resources
1006.98kB ( 1.34%) for other data
73.44MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 origins of code area: Top 10 object types in image heap:
12.68MB java.base 8.53MB byte[] for code metadata
4.24MB tomcat-embed-core-10.1.36.jar 3.95MB java.lang.Class
3.38MB java.xml 3.68MB java.lang.String
2.00MB jackson-databind-2.18.2.jar 3.30MB byte[] for java.lang.String
1.58MB spring-core-6.2.3.jar 3.23MB byte[] for general heap data
1.54MB spring-boot-3.4.3.jar 2.11MB byte[] for embedded resources
1.29MB svm.jar (Native Image) 1.39MB com.oracle.svm.core.hub.DynamicHubCompanion
983.58kB spring-web-6.2.3.jar 1.01MB byte[] for reflection metadata
910.85kB spring-beans-6.2.3.jar 741.69kB java.lang.String[]
876.34kB spring-webmvc-6.2.3.jar 597.07kB c.o.svm.core.hub.DynamicHub$ReflectionMetadata
7.23MB for 69 more packages 6.15MB for 3522 more object types
------------------------------------------------------------------------------------------------------------------------
Recommendations:
HEAP: Set max heap for improved and more predictable memory usage.
CPU: Enable more CPU features with '-march=native' for improved performance.
------------------------------------------------------------------------------------------------------------------------
10.3s (12.1% of total time) in 171 GCs | Peak RSS: 2.64GB | CPU load: 5.10
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
../springboot3-demo/graalvm-native-app/target/graalvm-native-app (executable)
========================================================================================================================
Finished generating 'graalvm-native-app' in 1m 24s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:38 min
[INFO] Finished at: 2025-03-03T16:41:31+08:00
[INFO] ------------------------------------------------------------------------

运行app

1
./graalvm-native-app

验证:

1
2
3
$ curl localhost:8080/hancher

Hello hancher

优缺点

先说优点

  • 去掉了jvm这一层,部署更加简单方便了
  • 通过app这种形式,原生级别支持了windows、mac、linux等主流操作系统
  • 启动更快,因为代码都编译成系统直接运行的二进制文件,不需要jvm的解析
    • 上面的helloWorld,使用jar启动,耗时0.9s,使用app启动,耗时0.18秒,将近5倍的差距。
  • 运行时占用内存更低,据统计,能减少50%左右

再说缺点

  • 打包很慢,会占用很高的内存和cpu
  • 对于反射等动态特性支持的不好,比如Class.forName(), 所以仅适用于一些简易项目,重量级项目现阶段还不太支持
  • 一个系统仅能打包本系统的app,不像go那样,一个系统能打包所有平台的app。
  • 包体积比原生的jar大了不少
    • 上面的helloWorld,jar大小空间20M,app大小73M, 大了3倍
    • 但如果算上jdk, 我统计了我安装后的jdk17大小300M,再加上jar, 使用jar的总体空间比app大了近5倍

总得来说,java在编译成本地app这一块,还有很大的空间,但已经完成了从0到1的进步,更适合云原生了,剩下的就是不断的优化了。

适用的场景:

  • 新项目,轻量级项目。老项目不太建议使用这个特性,因为动态特性的限制,可能会有一些功能验证不到
  • 类似云原生的Serverless架构,逻辑简单,更强调启动速度与内存占比的服务
  • 容器化部署的微服务等(native app 有容器版,下篇文章介绍)

参考

使用GraalVM打包系统可执行程序

github demo


springboot可以把项目打包成app了,Graal打包版
https://www.hancher.top/2025/03/04/spring-spring-native-image-graalvm/
作者
寒澈
发布于
2025年3月4日
许可协议