Spring Native 項目,把 Spring 項目編譯成原生程序!

Spring 發佈了 Spring Native 的 beta 版本,該功能已經在 start.spring.io 上可用了。

Spring Native 是什麼

Spring Native 可以通過 GraalVM 將 Spring 應用程序編譯成原生鏡像,提供了一種新的方式來部署 Spring 應用。Spring Native 支持 Java 和 Kotlin。

這個項目的目標是尋找 Spring JVM 的替代方案,提供一個能將應用程序打包,並運行在輕量級容器的方案。期望能夠在 Spring Native 中支持所有的 Spring 應用程序(幾乎不用修改代碼)。

優點

  • 編譯出來的原生 Spring 應用可以作爲一個獨立的可執行文件進行部署(不需要安裝 JVM)
  • 幾乎瞬時的啓動(一般小於 100 毫秒)
  • 瞬時的峯值性能
  • 更低的資源消耗

缺點

  • 比 JVM 更長的構建時間
  • 相比於傳統的 Java 運行方式,運行時優化不足

原生鏡像(native image)和常規 JVM 程序的區別

  • 在構建時會從主入口點,靜態分析應用程序
  • 在構建時會移除未使用的代碼
  • 需要配置反射、動態代理等
  • classpath 在構建時就已經確定
  • 沒有類延遲加載:可執行文件中所有的內容都會在啓動時加載到內存中
  • 在構建時就運行了一些代碼
  • 構建原生鏡像還存在一些 侷限性

前置條件:GraalVM

GraalVM 介紹起來篇幅比較長,這裏僅簡要介紹。官網:https://www.graalvm.org/

GraalVM 是一個高性能的多語言運行時環境。設計目的是能夠提高用 Java 和其他 JVM 語言編寫的應用程序的執行速度,同時還爲 JavaScript、Ruby、Python 和許多其他流行語言提供運行時。GraalVM 的多語言能力使得在一個應用程序中混合使用多種編程語言成爲可能,同時消除了不同語言間互相調用的成本。詳細內容可參考:Get Started with GraalVM

支持的語言

Specific Languages 1

Specific Languages 2

關鍵特性

關鍵特性

GraalVM 下的 Java 微服務

具體內容可參考:Lightweight cloud-native Java applications

Spring Native 的 Hello World

構建 Spring Boot native 應用程序有 2 種方式:

本文只介紹第一種。

系統要求

在待構建的機器上,必須安裝了 Docker,可以參考 Get Docker,同時注意要能夠以非 root 用戶啓動和運行。

可以通過使用 docker run hello-world (不包含sudo)命令檢查 Docker daemon 是否可用。

示例代碼

一個簡單的 Spring Boot Web 程序:

git clone https://github.com/spring-guides/gs-rest-service
cd gs-rest-service/complete

配置 Spring Boot 版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.4</version>
    <relativePath/>
</parent>

添加 Spring Native 依賴

org.springframework.experimental:spring-native 提供了 native 配置的 API,例如 @NativeHint 這些 Spring 運行成 native image 的註解類。

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.9.1</version>
    </dependency>
</dependencies>

添加 Spring AOT 插件

Spring AOT 插件執行代碼的提前轉換,用以修復 native image 的兼容性。

<build>
    <plugins>
        <!-- ... -->
        <plugin>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-aot-maven-plugin</artifactId>
            <version>0.9.1</version>
            <executions>
                <execution>
                    <id>test-generate</id>
                    <goals>
                        <goal>test-generate</goal>
                    </goals>
                </execution>
                <execution>
                    <id>generate</id>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

開啓 native image 支持

Spring Boot 的 Spring Boot Buildpacks support 可以將 Spring Boot 應用程序打包成一個容器。native image buildpack 可以通過 BP_NATIVE_IMAGE 環境變量開啓。

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <builder>paketobuildpacks/builder:tiny</builder>
            <env>
                <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
            </env>
        </image>
    </configuration>
</plugin>

Maven Repository

<repositories>
    <!-- ... -->
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>
<pluginRepositories>
    <!-- ... -->
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

構建本地應用程序

mvn spring-boot:build-image

通過此命令,可以創建一個使用 GraalVM native image compiler 構建的 Linux 容器,默認情況下,這個鏡像是在本地。

運行本地應用

可以使用 docker images 命令,查看鏡像:

使用 docker 啓動這個鏡像:

docker run --rm -p 8080:8080 rest-service:0.0.1-SNAPSHOT

本次啓動時間是47ms,而 JVM 的程序啓動一般都是1500ms左右。

啓動時間

現在服務已經啓動了,可以通過 localhost:8080/greeting 訪問服務。在瀏覽器中可以看到:

{"id":1,"content":"Hello, World!"}

可能遇到的問題

權限問題

若編譯時遇到下面的情況,則表明構建時沒有 docker 權限,如果配置一直不成功,可以直接在 mvn spring-boot:build-image 命令前加個 sudo

報錯 1

內存問題

若編譯時遇到下面的情況,是 OOM 問題,需要把 Docker 的內存改大(8G)。

報錯 2

以 Mac 的 Docker Client 設置爲例:

Spring Native 所遇到的問題

這部分參考自:雲原生時代,Java 的危與機

JVM 的程序運行時間長,是因爲存在虛擬機的初始化和類加載過程,如果將字節碼直接編譯成原生代碼,則可以徹底解決這些問題。同時因爲沒有即時編譯器在運行時編譯,所有代碼都在編譯期編譯和優化。因爲少了 Java 虛擬機、即時編譯器這些額外組件,原生程序也能夠省去它們原本消耗的內存資源和鏡像體積。

Java 支持提前編譯最大的困難,在於 Java 是一門動態鏈接的語言,它假設程序的代碼空間是開發的,允許在程序的任何時候通過類加載器去加載新的類,作爲程序的一部分。要進行提前編譯,就必須放棄這部分動態性,所有要運行的代碼必須在編譯期全部可知。這樣動態加載、反射(通過反射可以調用在編譯期不可知的方法)、動態代理、字節碼生成庫(如 CGLib)等一切會運行時產生新代碼的功能都不再可用。

  • 對於反射,需要用戶在編譯期,通過配置文件或編譯器參數的形式,明確告知編譯器程序代碼中哪些方法只通過反射來訪問的。
  • 用戶往往不知道動態生成字節碼的具體信息,這些只能由程序去做妥協。默認情況下,每一個 Spring 管理的 Bean 都要用到 CGLib。從 Spring Framework 5.2 開始增加了@proxyBeanMethods 註解來排除對 CGLib 的依賴,僅使用標準的動態代理去增強類。

當然 Spring Native 遇到的問題有很多,且仍然處於試驗階段。以原生方式運行後,啓動時間是能夠縮短很多,但是程序的運行效率還是若於傳統基於 JVM 的方式,且編譯成原生程序代碼的時間更長。

參考鏈接

GitHub LeetCode 項目

項目 GitHub LeetCode 全解,歡迎大家 star、fork、merge,共同打造最全 LeetCode 題解!

Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章