SpringBoot3.x原生鏡像-Native Image嘗鮮

前提

Spring團隊致力於為Spring應用程式提供原生映像支援已經有一段時間了。在SpringBoo2.xSpring Native實驗項目中醞釀了3年多之後,隨著Spring Framework 6Spring Boot 3的發布,對應的項目就是Spring Native,原生鏡像支援將會發布GA版本(換言之就是,Native Image相關支援會在Spring Boot 3GA版本中一起發布)。

前面這一段簡介摘抄自參考資料中的《Native Support in Spring Boot 3.0.0-M5》

筆者在寫這篇文章(2022-10-28)前後SpringBoot尚未發布3.x GA,版本3.0.0-M5+算是GA前相對穩定的版本,這裡選用當前3.x的最新非GA版本3.0.0-RC1進行調研。

spring-boot-native-image-1

什麼是Native Image

Native Image,這裡直譯為原生鏡像或者本地鏡像,是一種提前將(Java)程式碼編譯為二進位文件(原生可執行文件,native executable)的技術。原生可執行文件只包含運行時所需要的程式碼,即應用程式類、標準庫類、語言運行時和來自JDK的靜態鏈接的原生程式碼(也就是這樣的二進位文件可以直接運行,不需要額外安裝JDK)。由原生鏡像生成的可執行文件有幾個重要的優點:

  • 使用Java虛擬機所需資源的一小部分,因此運行成本更低
  • 啟動時間大幅度下降,以毫秒為單位
  • 不需要進行預熱即可提供最佳性能
  • 可以打包到輕量級容器映像中以便快速有效地部署
  • 減少了攻擊面(這個和網路安全相關)

Spring Boot 3使用GraalVM方案提供Native Image支援

安裝GraalVM

//www.graalvm.org/downloads – Download GraalVM頁面中下載對應作業系統的GraalVM

spring-boot-native-image-2

筆者開發環境使用的作業系統是Windows10,下載和選用下圖中的安裝包:

spring-boot-native-image-3

解壓完成後配置一下JAVA_HOMEGRAALVM_HOME並且把GRAALVM_HOME\bin添加到PATH中。完成後可以執行一下java -version進行驗證:

spring-boot-native-image-4

如果已經安裝了其他版本的JDK,先暫時全局替換為GraalVM,也就是JAVA_HOME、GRAALVM_HOME同時配置為GraalVM的解壓目錄,因為目前看來這樣做才能正常打包原生鏡像

確定GraalVM版本無誤,到此安裝完成。另外,需要配置好了Maven,建議重新安裝一個3.6.x+版本的Maven並且把MAVEN_HOME\bin添加到PATH中。

編寫應用程式

新建一個命名為spring-boot-native-imageMaven項目或者模組,選用剛才下載好的GraalVM

spring-boot-native-image-5

項目的POM文件引入下面幾組依賴:

  • spring的快照repository,因為需要下載RC1版本依賴,暫時不能從中央倉庫拉取
  • spring-boot-starter-parent,定義版本為RC1
  • native-maven-plugin插件,用於原生鏡像打包
  • spring-boot-starter-web,用於構建一個簡單的web項目
<!-- spring-boot-starter-parent -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.0-RC1</version>
</parent>

<!-- repository -->
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <url>//repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <url>//repo.spring.io/milestone</url>
    </repository>
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>spring-snapshots</id>
        <url>//repo.spring.io/snapshot</url>
    </pluginRepository>
    <pluginRepository>
        <id>spring-milestones</id>
        <url>//repo.spring.io/milestone</url>
    </pluginRepository>
</pluginRepositories>

<!-- spring-boot-starter-web -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<!-- native-maven-plugin -->
<plugins>
    <plugin>
        <groupId>org.graalvm.buildtools</groupId>
        <artifactId>native-maven-plugin</artifactId>
        <version>0.9.16</version>
        <extensions>true</extensions>
        <executions>
            <execution>
                <id>build-native</id>
                <goals>
                    <goal>compile-no-fork</goal>
                </goals>
                <phase>package</phase>
            </execution>
            <execution>
                <id>test-native</id>
                <goals>
                    <goal>test</goal>
                </goals>
                <phase>test</phase>
            </execution>
        </executions>
        <configuration>
            <mainClass>cn.vlts.NativeApplication</mainClass>
            <imageName>native-app</imageName>
            <buildArgs>
                <buildArg>--verbose</buildArg>
            </buildArgs>
        </configuration>
    </plugin>
</plugins>

最終的POM文件看起來如下:

spring-boot-native-image-6

項目中只有一個啟動類cn.vlts.NativeApplication,編寫了main方法和一個用於集成測試的控制器方法:

@RestController
@SpringBootApplication
public class NativeApplication {

    public static void main(String[] args) {
        SpringApplication.run(NativeApplication.class, args);
    }

    @GetMapping(path = "/hello")
    public ResponseEntity<String> hello() {
        return ResponseEntity.ok("world");
    }
}

打包和調試

完成項目配置和程式碼編寫後,執行下面的Maven命令進行打包:

mvn package -Pnative

打包過程可能會遇到下面的問題:

  • 最有可能的問題:Default native-compiler executable 'cl.exe' not found via environment variable PATH

解決方案在Stackoverflow對應的問題回答中找到:

spring-boot-native-image-7

其實就是在Window作業系統開發環境下基於GraalVM構建原生鏡像依賴Microsoft Visual C++ (MSVC),建議安裝MSVC 2017 15.5.5+,可以安裝Visual Studio (2019)並且安裝對應的MSVC

因為很早之前筆者在調試Rust時候已經安裝過Visual Studio 2019用於其debug工具鏈,這裡無須進行安裝。在安裝Visual Studio勾選MSVC vXXX的組件進行安裝即可,然後需要把對應的MSVC工具的bin目錄添加到PATH中(這個目錄一般是VS_HOME\VC\Tools\MSVC\版本號\bin\Hostx64\x64):

spring-boot-native-image-8

  • 其次可能遇到的問題:打包過程出現stdio.h庫文件報錯或者找不到主類Main entry point class 'app.jar' not found x.y.Application

其實還是因為MSVC的問題,在GraalVM文檔中有提示如下:

spring-boot-native-image-9

簡單來說就是必須在Visual Studio自帶的命令行工具x64 Native Tools Command Prompt中執行native image相關命令,這個命令行工具初始化如下:

spring-boot-native-image-10

x64 Native Tools Command Prompt中先進入目標項目根目錄,然後執行mvn -Pnative package

spring-boot-native-image-11

最終看到BUILD SUCCESS字眼,項目的target目錄下可以看到一個.exe和一個.jar文件,而.exe文件就是前面一直提到的可執行的二進位文件

spring-boot-native-image-14

直接運行它:

spring-boot-native-image-12

可以看到這個文件運行完全不依賴外部Java虛擬機,並且啟動速度極快(600毫秒左右),可以用POSTMAN等工具測試程式介面:

spring-boot-native-image-13

到此可以驗證程式功能正常。

小結

SpringBoot3.x原生鏡像正式發布後會是SpringBoot在雲原生領域的一個巨大進步,讓我們拭目以待。但是就目前來看,常用的Windows開發環境下想要嘗試native image技術需要解決比較多的問題,LinuxUnix平台下尚未驗證,希望後面的版本迭代能夠降低使用難度並且支援一個命令多平台打包的功能。

參考資料:

(本文完 c-2-d e-a-20221030)