前提
之前曾经写过一篇《SpringBoot3.x 原生镜像-Native Image 尝鲜》,当时SpringBoot
处于3.0.0-M5
版本,功能尚未稳定。这次会基于SpringBoot
当前最新的稳定版本3.1.2
详细分析Native Image
的实践过程。系统或者软件版本清单如下:
组件 | 版本 | 备注 |
---|---|---|
macOS Ventura | 13.4.1(c) | ARM 架构 |
sdkman | 5.18.2 | JDK 和各类SDK 包管理工具 |
Liberica Native Image Kit | 23.0.1.r17-nik | 可以构建Native Image 的JDK |
SpringBoot | 3.1.2 | 使用当前(2023-08-20 )最新发布版 |
Maven | 3.9.0 | - |
安装 sdkman
sdkman是一个轻量级、支持多平台的开源开发工具管理器,可以通过它安装任意主流发行版本(例如OpenJDK
、Kona
、GraalVM
等等)的任意版本的JDK
。通过下面的命令可以轻易安装sdkman
:
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk version
可以通过sdk list java
查看支持的JDK
发行版本:
通过shell
命令sdk install java $Identifier
就可以安装对应的JDK
发行版。例如可以这样安装GraalVM-ce-17
:
sdk install java 17.0.8-graalce
通过shell
命令sdk uninstall java $Identifier
可以卸载对应的JDK
发行版。如果安装了多个版本或者多个发行版的JDK
,可以通过shell
命令sdk default java $Identifier
去指定默认使用的JDK
版本,例如:
sdk default java 17.0.8-graalce
可以通过shell
命令sdk current
或者sdk current java
查看当前正在使用的SDK
或者JDK
版本。
安装 Liberica NIK
Liberica Native Image Kit
是bellsoft
出品的旨在创建高性能本地二进制(Native Binaries
)基于JVM
编写的应用的工具包,简称为Liberica NIK
。Liberica NIK
本质就是把OpenJDK
和多种其他工具包一起封装起来的JDK
发行版,在Native Image
功能应用过程,可以简单把它视为OpenJDK
+ GraalVM
的结合体。可以通过sdk list java
查看相应的JDK
版本:
这里选择JDK-17
的版本进行安装:
sdk install java 23.0.1.r17-nik
# 这里最好把此JDK设置为当前系统的默认JDK,否则后面编译镜像时候会提示找不到GraalVM
sdk default java 23.0.1.r17-nik
安装完成后,通过java -version
验证一下:
编写 SpringBoot 应用
基于Maven
新建一个SpringBoot
应用,这里已经整理好了一份POM
文件,实践过程可以直接用,如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.vlts</groupId>
<artifactId>spring-boot-native-image-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.version>3.11.0</maven.compiler.version>
<maven.install.version>3.1.1</maven.install.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat.experimental</groupId>
<artifactId>tomcat-embed-programmatic</artifactId>
<version>${tomcat.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<version>${maven.compiler.version}</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>${maven.install.version}</version>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>cn.vlts.NativeImageApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
这里把Maven
的所有插件都提升到当前()最新版本,原生镜像打包的关键插件是native-maven-plugin
,此插件是跟随spring-boot-starter-parent
进行版本管理,这里无须指定插件的版本。另外,tomcat-embed-programmatic
是一个实验性依赖,可以降低嵌入式Tomcat
的内存使用,在生产中应用时候可以暂不启用此特性。接着编写启动类cn.vlts.NativeImageApplication
:
@SpringBootApplication
@RestController
public class NativeImageApplication {
public static void main(String[] args) {
SpringApplication.run(NativeImageApplication.class, args);
}
@RequestMapping(path = "/")
public ResponseEntity<String> index() {
return ResponseEntity.ok("index");
}
}
构建、测试与发布
三个操作的Maven
命令分别是:
构建:
mvn -Pnative native:compile
测试:
mvn -PnativeTest test
发布:
mvn -Pnative spring-boot:build-image
,注意此命令会打包镜像并且发布到Docker
的官方仓库中
❝虽然 native:compile 命令表面意义是编译,但是实际上它就是构建原生镜像的命令
❞
执行构建流程:
mvn -Pnative native:compile -Dmaven.test.skip=true
构建结果如下:
其中这个不带.jar
后缀的就是最终的原生镜像,并且Native Image
是不支持跨平台的,它只能在ARM
架构的macOS
中运行(受限于笔者的编译环境)。可以发现它(见上图中的target/spring-boot-native-image-demo
,它是一个二进制执行文件)的体积比executable jar
大好几倍。参照SpringBoot
的官方文档,经过AOT
编译的SpringBoot
应用会生成下面的文件:
Java
源代码字节码(例如动态代理编译后的产物等)
GraalVM
识别的提示文件:资源提示文件(
resource-config.json
)反射提示文件(
reflect-config.json
)序列化提示文件(
serialization-config.json
)Java
(动态)代理提示文件(proxy-config.json
)JNI
提示文件(jni-config.json
)
这里的输出非执行包产物基本都在target/spring-aot
目录下,其他非Spring
或者项目源代码相关的产物输出到graalvm-reachability-metadata
目录中。最后可以验证一下产出的Native Image
:
可以看到启动速度达到惊人的毫秒级别,如果应用在生产中应该可以全天候近乎无损发布。当然,理论上Native Image
性能也会大幅度提升,但是限于篇幅这里暂时不进行性能测试。
小结
鉴于SpringBoot3.x
的正式版已经推出一段时间,从文档上看,Native Image
使用的技术已经相对成熟,能够用于生产条件。当然,Native Image
目前还存在一些局限性会让一些组件完全无法使用或者部分功能受限(参考Spring Boot with GraalVM),希望这些问题或者局限性有一天能够突破让所有JVM
应用迎来一次性能飞跃。
(本文完 c-2-d e-a-20230820)