SpringBoot3.x原生鏡像-Native Image實踐
前提
之前曾經寫過一篇《SpringBoot3.x 原生鏡像-Native Image 嘗鮮》,當時SpringBoot處于3.0.0-M5版本,功能尚未穩(wěn)定。這次會基于SpringBoot當前最新的穩(wěn)定版本3.1.2詳細分析Native Image的實踐過程。系統(tǒng)或者軟件版本清單如下:
| 組件 | 版本 | 備注 |
|---|---|---|
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)最新發(fā)布版 |
Maven |
3.9.0 |
- |
安裝 sdkman
sdkman是一個輕量級、支持多平臺的開源開發(fā)工具管理器,可以通過它安裝任意主流發(fā)行版本(例如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發(fā)行版本:
通過shell命令sdk install java $Identifier就可以安裝對應的JDK發(fā)行版。例如可以這樣安裝GraalVM-ce-17:
sdk install java 17.0.8-graalce
通過shell命令sdk uninstall java $Identifier可以卸載對應的JDK發(fā)行版。如果安裝了多個版本或者多個發(fā)行版的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出品的旨在創(chuàng)建高性能本地二進制(Native Binaries)基于JVM編寫的應用的工具包,簡稱為Liberica NIK。Liberica NIK本質就是把OpenJDK和多種其他工具包一起封裝起來的JDK發(fā)行版,在Native Image功能應用過程,可以簡單把它視為OpenJDK + GraalVM的結合體??梢酝ㄟ^sdk list java查看相應的JDK版本:
這里選擇JDK-17的版本進行安裝:
sdk install java 23.0.1.r17-nik
# 這里最好把此JDK設置為當前系統(tǒng)的默認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");
}
}
構建、測試與發(fā)布
三個操作的Maven命令分別是:
-
構建: mvn -Pnative native:compile -
測試: mvn -PnativeTest test -
發(fā)布: mvn -Pnative spring-boot:build-image,注意此命令會打包鏡像并且發(fā)布到Docker的官方倉庫中
?雖然 native:compile 命令表面意義是編譯,但是實際上它就是構建原生鏡像的命令
?
執(zhí)行構建流程:
mvn -Pnative native:compile -Dmaven.test.skip=true
構建結果如下:
其中這個不帶.jar后綴的就是最終的原生鏡像,并且Native Image是不支持跨平臺的,它只能在ARM架構的macOS中運行(受限于筆者的編譯環(huán)境)??梢园l(fā)現(xiàn)它(見上圖中的target/spring-boot-native-image-demo,它是一個二進制執(zhí)行文件)的體積比executable jar大好幾倍。參照SpringBoot的官方文檔,經過AOT編譯的SpringBoot應用會生成下面的文件:
-
Java源代碼 -
字節(jié)碼(例如動態(tài)代理編譯后的產物等) -
GraalVM識別的提示文件: -
資源提示文件( resource-config.json) -
反射提示文件( reflect-config.json) -
序列化提示文件( serialization-config.json) -
Java(動態(tài))代理提示文件(proxy-config.json) -
JNI提示文件(jni-config.json)
這里的輸出非執(zhí)行包產物基本都在target/spring-aot目錄下,其他非Spring或者項目源代碼相關的產物輸出到graalvm-reachability-metadata目錄中。最后可以驗證一下產出的Native Image:
可以看到啟動速度達到驚人的毫秒級別,如果應用在生產中應該可以全天候近乎無損發(fā)布。當然,理論上Native Image性能也會大幅度提升,但是限于篇幅這里暫時不進行性能測試。
小結
鑒于SpringBoot3.x的正式版已經推出一段時間,從文檔上看,Native Image使用的技術已經相對成熟,能夠用于生產條件。當然,Native Image目前還存在一些局限性會讓一些組件完全無法使用或者部分功能受限(參考Spring Boot with GraalVM),希望這些問題或者局限性有一天能夠突破讓所有JVM應用迎來一次性能飛躍。
(本文完 c-2-d e-a-20230820)
