環境準備
作業系統: MacOS Monterey 12.5.1
CPU: Intel I7
安裝java17
- 從Oracle下載java17對應版本,並安裝在Mac系統中
- 設定環境變數便於快速切換shell的環境。以當前使用者的zsh為例,當前使用者home下的
.zshrc
檔案中增加內容
# 指定java17的home目錄
export JAVA_17_HOME='/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/
Home'
# 快速將JAVA的HOME指定為java17的home目錄,系統上安裝多jdk版本時好用
alias java17="export JAVA_HOME=$JAVA_17_HOME"
# 設定maven別名,在使用maven命令時先設定當前shell的java環境
alias mvn17='java17;mvn '
在IDE中開發程式碼直接指定目錄設定專案的JDK版本為java17即可,建議使用最新版本的IDEA
安裝Graalvm
- 下載對應系統對應JDK版本的Graalvm,下載頁面地址: https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-22.3.1
設定Graalvm的home目錄
# 將Graalvm的home路徑新增到系統變數中 export GRAALVM_HOME='/Users/{userName}/{path}/graalvm-ce-java17-amd64/Contents/Home' # 將graalvm的bin目錄新增到系統path中,可以直接使用bin下的命令,不再需要完整的路徑 export PATH=$GRAALVM_HOME/bin:$PATH
安裝
native-image
$gu install native-image # 或者使用命令全路徑 $GRAALVM_HOME/bin/gu install native-image
最簡示例程式碼
專案的程式碼目錄如下:
POM.xml檔案
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 繼承自Springboot的parent,因此內建了native的profile及plugin等資訊 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.followtry.app</groupId>
<artifactId>spring-image-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-image-demo</name>
<description>測試Spring的native image</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 構建原生映象的外掛 -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<imageName>followtry-image</imageName>
<buildArgs>
<!-- 開發時可使用,加快構建速度,部署時需要去掉 -->
<buildArg>-Ob</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
jAVA程式碼
應用啟動入口類:
package cn.followtry.app.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FollowtryImageApplication {
public static void main(String[] args) {
SpringApplication.run(FollowtryImageApplication.class, args);
}
}
測試用的Service
package cn.followtry.app.demo.service;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* @author followtry
* @since 2023/2/24 16:55
*/
@Service
public class HelloService {
private static final Logger log = LoggerFactory.getLogger(HelloService.class);
@PostConstruct
public void init() {
System.out.println("HelloService.init");
log.info("HelloService.init");
}
public String sayHello(String name) {
String msg = "hello," + name;
log.info(msg);
return msg;
}
}
controller如下:
package cn.followtry.app.demo.web;
import cn.followtry.app.demo.service.HelloService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author followtry
* @since 2023/2/25 20:18
*/
@RestController
@RequestMapping("test")
public class HelloController {
private final HelloService helloService;
public HelloController(HelloService helloService) {
this.helloService = helloService;
}
@GetMapping("hello")
public String hello(String name) {
return helloService.sayHello(name);
}
}
為了支援java17的編譯,需要對maven新增編譯引數.如目錄.mvn
下的jvm.config
內容如下:
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED
--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED
--add-opens=java.base/java.nio=ALL-UNNAMED
-Dio.netty.tryReflectionSetAccessible=true
可以看出,應用程式碼基本沒什麼特別之處,但就這樣的普通程式碼就可以最終被編譯成本地可執行的映象檔案。
編譯打包
原jar包的打包方式
需執行命令mvn17 clean package
(mvn17來自於文章初始部分自定義的alias),編譯後的jar檔案spring-image-demo-0.0.1-SNAPSHOT.jar
大小為20M,且打包耗時3.96
,如圖
執行命令java -jar ./target/spring-image-demo-0.0.1-SNAPSHOT.jar
啟動java應用,從圖中可以看出應用啟動完耗時2.38
秒,介面/test/hello?name=zhangsan
可以正常訪問。
native image打包方式
執行命令mvn17 clean native:compile -Pnative
,經歷步驟日誌的關鍵資訊包括
Scanning for projects...
maven-clean-plugin:3.2.0:clean
# native 編譯前先執行類可達性分析,將需要編譯的類重新生成後設資料資訊
native-maven-plugin:0.9.20:compile
native-maven-plugin:0.9.20:add-reachability-metadata
maven-resources-plugin:3.3.0:resources
maven-compiler-plugin:3.10.1:compile
maven-resources-plugin:3.3.0:testResources
maven-compiler-plugin:3.10.1:testCompile
maven-surefire-plugin:2.22.2:test
spring-boot-maven-plugin:3.0.3:process-aot
maven-jar-plugin:3.3.0:jar
spring-boot-maven-plugin:3.0.3:repackage
# 需要的類後設資料資訊重新生成完後,開始執行Native編譯
native-maven-plugin:0.9.20:compile
GraalVM Native Image: Generating 'followtry-image' (executable)...
[1/7] Initializing... cost 11.4s
[2/7] Performing analysis cost 53.2s
[3/7] Building universe... cost 5.4s
[4/7] Parsing methods... cost 5.5s
[5/7] Inlining methods... cost 2.3s
[6/7] Compiling methods.. cost 21.1s
[7/7] Creating image... cost 9.1s
Finished generating 'followtry-image' in 1m 56s.
Total time: 02:07 min
經過本地映象編譯後,生成的followtry-image
可執行檔案大小為68M
,位元組碼編譯後的jar包大小為20M
。如圖:
應用啟動資訊:
兩種方式的對比資訊
原Jar方式 | Native Image方式 | 對比倍數 | |
---|---|---|---|
編譯時間 | 3.96s | 127s | Native編譯慢32倍 |
啟動時間 | 2.38s | 0.13s | Native啟動時間快18倍 |
編譯後大小 | 20M | 68M | Native包是Jar包的3.4倍 |
如文章(http://george5814.github.io/2023-02-24/spring-aot.html)中所說,SpringAOT在執行後會生成Java類對應的BeanDefinition的class資訊,該步驟是在process-aot
時完成的。將打好的jar包解壓後可以看到如圖中增加的幾種位元組碼檔案,該檔案即為將註解解析後生成的類編譯而成,是為了在graalvm執行native 編譯時類一定存在。
另一個比較關鍵的是在META-INF
中生成的反射、資源等的配置檔案。已反射的配置檔案reflect-config.json
為例,如下圖中示例,可看出SpringBoot的maven外掛已經自定找到反射類資訊並將其作為配置進行生成。
結語
本文主要講解了從環境安裝,程式碼編寫,編譯啟動,打包方式對比等方面簡單介紹了SpringBoot3.0 在native image的入門使用,其中的AOT原理機制解析待後續文章繼續輸出。
參考文章
https://www.baeldung.com/spring-native-intro#overview-1
https://graalvm.github.io/native-build-tools/latest/graalvm-s...
https://github.com/graalvm/graalvm-demos/tree/master/spring-native-image