SpringBoot.3中的aot.factories到底有什麼用?和以前的spring.factories一樣嗎?

努力的小雨發表於2024-10-08

首先,我們來澄清一下 aot.factoriesspring.factories 之間的區別。這兩個檔案不僅名稱不同,而且在功能上也存在顯著差異。接下來,我們將深入探討這兩個檔案的具體作用以及它們各自的應用場景。讓我們一起來揭開它們的神秘面紗吧!

在我們上一次討論 Spring Boot 3 版本時,我們關注了它的載入機制並注意到了一些小的變化。嚴格來說,這些變化主要體現在檔名稱的調整上:原本的 META-INF/spring.factories 檔案已經遷移至新的位置,即 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

想要了解更多詳細資訊,歡迎查閱這篇文章:https://www.cnblogs.com/guoxiaoyu/p/18384642

問題來了

要深入瞭解 Spring Boot 的載入機制,首先需要認識到每個第三方依賴包實際上都包含自己獨特的 META-INF/spring.factories 檔案。正如圖中所示。

這些檔案在應用程式啟動時扮演著重要的角色,它們定義了自動配置的類和其他相關設定,幫助 Spring Boot 在執行時自動識別和載入相應的配置。

image

然而,當我們試圖檢視某個第三方依賴包時,可能會發現找不到相應的 META-INF/spring.factories 檔案,甚至沒有 *.imports 檔案,這時該怎麼辦呢?不要慌張!並不是所有的專案都具備自動配置功能。例如,ZhiPuAiAutoConfiguration 的自動配置實際上已經包含在 Spring Boot 的核心庫中。

@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(ZhiPuAiApi.class)
@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class,
        ZhiPuAiEmbeddingProperties.class, ZhiPuAiImageProperties.class })
public class ZhiPuAiAutoConfiguration {
}

可以簡單理解為,一旦你引用了相應的依賴包,它們的配置便會立即生效。然而,在查詢配置的 *.imports 檔案時,我發現了一個有趣的現象:許多依賴包下也存在 aot.factories 檔案。這是用來做什麼的呢?考慮到 Spring Boot 自身也包含此類檔案,這表明這個概念並非無的放矢。

因此,帶著這個疑問,我決定深入探究其背後的機制與作用。

一探究竟

經過了一番 AI 問答和上網搜尋,我大致瞭解了 aot.factories 檔案的用途:它實際上是為打包和編譯服務的。這個檔案可以幫助將 Java 專案打包成可執行的 EXE 檔案(在 Windows 系統下,其他作業系統則有不同的打包方式),這樣就無需依賴 Java 執行環境即可直接執行。不過,這與 Spring Boot 的自動配置機制並沒有直接關係。

那麼,為什麼會發明這樣的東西呢?我知道你很著急,但是你先彆著急!聽我一點一點講,你就更明白了!

image

Java當前痛點

有過 Java 開發經驗的朋友們應該都知道,以前的 Java 應用通常都是單體架構,這意味著啟動一個專案往往需要耗費幾分鐘的時間,尤其是大型專案,啟動時間更是讓人頭疼。因此,隨著技術的發展,微服務架構應運而生,不僅顯著縮短了啟動時間,而且將業務邏輯進行了合理的切分。

然而,微服務架構也並非沒有缺點。儘管啟動速度更快,專案在啟動後往往無法立即達到最佳的運轉狀態,也就是說,應用需要一段時間才能進入高效的執行峰值。

因為 Java 的底層通常使用的是 HotSpot 虛擬機器,HotSpot 的執行機制是將常用的程式碼部分編譯為本地(即原生)程式碼。這意味著在程式啟動之初,HotSpot 並不知道哪些程式碼會成為“熱點”程式碼,因此無法立即將這些程式碼轉換為機器能夠直接理解和執行的形式。

在這個過程中,HotSpot 會不斷分析和監測程式碼的執行情況,以快速識別出哪些部分是頻繁被呼叫的。只有在識別出熱點程式碼並將其編譯為原生代碼之後,我們的專案才能實現最佳的吞吐量。

要想讓 Java 像 Python 那樣實現瞬時啟動,幾乎是不可能的。這一現象使得 Java 在很多情況下更適合用於企業級服務,主要原因在於其所追求的穩定性和可靠性。在企業環境中,系統的穩定性往往是首要考慮的因素。

然而,Java 也面臨著一系列挑戰,這些挑戰在雲端計算時代尤為突出。

雲時代

以 Serverless 為例,Serverless 是一種在雲端計算環境中日益成為主流的部署模式。它透過將基礎設施的管理和運維任務抽象化,使開發者能夠更加專注於業務邏輯的實現,而不必過多關注底層的資源配置和管理。

image

我就不提以前那種需要自己部署物理機的老年代的情況了。如今,絕大多數公司都已經採用了 Kubernetes(K8s)作為叢集管理的解決方案。在各大雲服務提供商處購買伺服器後,企業通常會自行管理其叢集服務。運維團隊則負責監控和最佳化資源配置,及時進行擴充套件以滿足需求。

此外,隨著技術的發展,Server Mesh 和邊車模式也逐漸興起,這些都是值得深入瞭解的概念。歸根結底,這些改進的目的就是為了顯著節省公司內部的開發時間,從而讓團隊能夠更專注於核心業務。

目前的 Serverless 架構顯著提高了資源利用的效率,因為所有的基礎設施管理工作都由雲服務提供商負責。實際上,雲廠商的基礎設施本身並沒有發生根本變化,變化的主要是架構設計,使得客戶的使用體驗更加便捷和高效。在這種模式下,無論是運維人員還是開發人員,都只需關注函式的部署,而無需深入瞭解伺服器的細節資訊。

開發者不再需要關心函式的執行方式、底層有多少容器或伺服器在支撐這些服務。對他們而言,這一切都被抽象化為一個簡單的介面,只需確保引數對接得當即可。

但是,你敢用 Java 來部署 Serverless 函式嗎?當系統的吞吐量急劇上升,需要迅速啟動一個新節點來支撐額外的負載時,這位 Java 大哥可能還在忙著啟動或者進行預熱,這可真是耽誤事啊!所以Java作為牛馬屆的老大哥怎麼可能會願意當小弟?

image

GraalVM 簡介

如果你還不熟悉 GraalVM,但一定聽說過 OpenJDK。實際上,它們都是完整的 JDK 發行版本,能夠執行任何面向 JVM 的語言開發的應用。不過,GraalVM 不僅限於此,它還提供了一項獨特的功能——Native Image 打包技術。這項技術的強大之處在於,它能夠將應用程式打包成可以獨立執行的二進位制檔案,這些檔案是自包含的,完全可以脫離 JVM 環境執行。

換句話說,GraalVM 允許你建立類似於常見的可執行檔案(如 .exe 檔案)的應用程式,這使得部署和分發變得更加簡便和靈活。

image

如上圖所示,GraalVM 編譯器提供了兩種模式:即時編譯(JIT)和提前編譯(AOT)。AOT全稱為Ahead-of-Time Processing。

對於 JIT 模式,我們都知道,Java 類在編譯後會生成 .class 格式的檔案,這些檔案是 JVM 可以識別的位元組碼。在 Java 應用執行的過程中,JIT 編譯器會將一些熱點路徑上的位元組碼動態編譯為機器碼,以實現更快的執行速度。這種方法充分利用了執行時資訊,能夠根據實際的執行情況進行最佳化,從而提高了效能。

而對於 AOT 模式,GraalVM 則在編譯期間就將位元組碼轉換為機器碼,完全省去了執行時對 JVM 的依賴。由於省去了 JVM 載入和位元組碼執行期預熱的時間,AOT 編譯和打包的程式具有非常高的執行時效率。這意味著在啟動時,應用程式可以幾乎瞬間響應,極大地提高了處理請求的能力。

那麼,這種 AOT 編譯到底有多快?它是否會成為 Serverless 函式的一種常用方案,超越 Python 等其他語言的應用呢?為了驗證其效能優勢,我們可以進行實際測試。

image

安裝GraalVM

我們大家基本上都在本地安裝了 IntelliJ IDEA 開發工具,使用起來非常方便。在這裡,我們可以直接透過 IDEA 的內建功能下載 GraalVM,省去了在官方網站上尋找和下載的時間。只需簡單幾步,我們就可以快速獲取到最新的 GraalVM 版本,隨時準備進行開發。

下載完成後,我們只需配置專案的 JDK 為 GraalVM。由於我目前使用的是 JDK 17,因此需要選擇與之相容的 GraalVM 17 版本。這種配置過程相對簡單,只需在專案設定中更改 JDK 路徑即可。

image

我們將繼續使用之前研究過的 Spring AI 專案,在此基礎上,我們需要新增一些相關的 Spring Boot 外掛。

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
</plugin>

在我們順利完成所有配置後,準備進行編譯時,卻意外地遇到了錯誤提示,顯示 JAVA_HOME 指向的是我們原本的 JDK 1.8。這一問題的出現主要是由於其中某個工具並不依賴於 IntelliJ IDEA 的啟動變數,而是直接讀取了 JAVA_HOME 的環境變數。

為了解決這個問題,我們需要確保 JAVA_HOME 環境變數正確指向我們新安裝的 GraalVM 版本。因此,我們必須在本地系統中下載並安裝 GraalVM,確保其版本與我們專案中所需的 JDK 版本相匹配。

首先我們找到官網:https://www.graalvm.org/downloads/

image

因為我是windows版本,所以自己請選擇好相應的作業系統。等待下載完畢,解壓完成後,將配置環境變數指向改目錄後重啟生效,再次編譯即可。

執行後,還是報錯如下:

Error: Please specify class (or /) containing the main entry point method. (see --help)

內容就是找不到啟動類的意思,所以需要加一些配置。找了半天修改如下,切記前後順序別變,否則還是會有點問題。

<build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <configuration>
                    <!-- imageName用於設定生成的二進位制檔名稱 -->
                    <imageName>${project.artifactId}</imageName>
                    <!-- mainClass用於指定main方法類路徑 -->
                    <mainClass>com.example.demo.DemoApplication</mainClass>
                    <buildArgs>
                        --no-fallback
                    </buildArgs>
                </configuration>
                <executions>
                    <execution>
                        <id>build-native</id>
                        <goals>
                            <goal>compile-no-fork</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

接下來,我們將繼續使用常用的 Maven 命令,如 mvn clean package,來進行專案的打包。這個過程可能會顯得有些漫長,尤其是與以前相比,打包速度似乎下降了不止一個級別。這次,我的打包過程持續了大約十分鐘,這確實比我之前的體驗慢了不少。

image

然後非常激動的點選了生成好的demo.exe檔案,結果還是在報錯:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.1)

Application run failed
org.springframework.boot.AotInitializerNotFoundException: Startup with AOT mode enabled failed: AOT initializer com.example.demo.DemoApplication__ApplicationContextInitializer could not be found
        at org.springframework.boot.SpringApplication.addAotGeneratedInitializerIfNecessary(SpringApplication.java:443)
        at org.springframework.boot.SpringApplication.prepareContext(SpringApplication.java:400)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
        at com.example.demo.DemoApplication.main(DemoApplication.java:10)

然後經過仔細查詢,需要指定profile為native,因為之前打的包沒有Aot資訊。

image

好的,又經歷了15分鐘打包完畢,這次,我們終於成功了,我們可以直觀的看下AOT方式打包後的啟動時間與jar包方式的啟動時間對比。簡直是天壤之別。

image

哦?確實,現在的啟動時間已經縮短到毫秒級別,真令人驚訝!透過 GraalVM 的 Native Image 技術,我們不僅實現了 Java 專案的快速啟動,還有效去除了傳統 Java 應用中的預熱時間。這一切看似都非常理想,然而,問題也隨之而來。

GraalVM 缺點

說完了 GraalVM 能解決 Java 原來的問題後,我們也必須認識到,它並非沒有缺點。如果沒有這些不足,大家對 GraalVM 的瞭解程度肯定會超過對 OpenJDK 的熟悉度。畢竟,既然它如此出色,為什麼大多數人卻沒有廣泛使用呢?

首先,相容性問題是一個顯著的挑戰。許多老舊版本的 JDK 專案根本無法與 GraalVM 相容,這無疑限制了大部分企業的使用範圍。對於那些依賴於較舊 JDK 的企業而言,遷移到 GraalVM 可能需要耗費大量時間和資源,甚至面臨重構程式碼的風險。

其次,即便是使用新版本 JDK 的專案,開發者們也往往對使用 GraalVM 感到猶豫。原因在於,GraalVM 對某些動態特性的支援相對較弱。例如,反射機制、資源載入、序列化以及動態代理等功能的限制,可能會對現有程式碼的執行產生重大影響。這些動態行為在許多應用程式中都是核心部分,任何對它們的削弱都可能導致功能缺失或效能問題。

image

有人可能會產生疑問:Spring 框架本身依賴於工廠模式和各種動態代理功能,若 GraalVM 不支援這些高階特性,豈不是意味著 Spring 的執行將受到致命影響?如果動態代理無法正常使用,Spring 的許多核心功能將會受到制約,那剛才提到的打包成功又是怎麼回事呢?

實際上,這一切的背後得益於 GraalVM 提供的 AOT(Ahead-of-Time)後設資料檔案功能。這個特性使得開發者能夠在編譯階段明確哪些類和方法將會使用到動態代理,GraalVM 會在編譯時將這些資訊整合到最終的可執行檔案中。

RuntimeHints與aot.factories

GraalVM 的 API —— RuntimeHints 負責在執行時收集反射、資源載入、序列化和 JDK 代理等功能的需求。這一特性為我們理解 GraalVM 如何支援動態特性提供了重要線索。實際上,大家在這裡就可以猜到 aot.factories 檔案的作用。沒錯,這個檔案的存在正是為了在 GraalVM 編譯時,確保能夠載入 Spring 框架所需的 JDK 代理相關需求。我們看下檔案:

org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider.FreeMarkerTemplateAvailabilityRuntimeHints,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider.GroovyTemplateAvailabilityRuntimeHints,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints,\
org.springframework.boot.autoconfigure.template.TemplateRuntimeHints

org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingProcessor

org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.autoconfigure.flyway.ResourceProviderCustomizerBeanRegistrationAotProcessor

org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer

我們可以注意到 RuntimeHintsRegistrar 的存在,它的主要作用是識別並載入所有實現了相關介面的類,從而進行解析和處理。需要強調的是,GraalVM 預設並不會自動查詢 aot.factories 檔案,因為這屬於 Spring 的特定機制。這就意味著,如果沒有顯式的指引,GraalVM 是無法主動識別和利用這些動態特性。

在 RuntimeHintsRegistrar 下面,我們還可以看到許多 AotProcessor 的實現。這個結構和我們之前討論的 beanFactoryProcessor 有些相似,但這裡我們不深入探討具體的細節。今天我們只聚焦於表面現象,以便理解其基本功能。

Spring 實際上已經為我們解決了載入相關資訊的問題,使得動態特性可以在編譯時得到適當處理。然而,這並不意味著一切都已經準備就緒。第三方元件同樣需要提供相應的實現,以確保與 Spring 的相容性。如果你的依賴庫使用了某些高階功能,但沒有實現 Spring 的 aot.factories 掃描機制,那麼這些功能在編譯後將無法生效。

因此,仍然有許多工作需要進行,以確保整個生態系統的相容性和功能性。

總結

在探索 aot.factoriesspring.factories 的過程中,我們不僅揭示了這兩個檔案的本質差異,還深入探討了它們在 Spring Boot 3 中的作用及其應用場景。這一探索之旅引領我們進入了現代 Java 應用開發的前沿,尤其是在 Serverless 和微服務架構的背景下。隨著雲端計算的發展,應用程式的效能與啟動速度已成為開發者的核心關注點。在此背景下,GraalVM 的出現提供了一種新穎的解決方案,透過其 Native Image 功能,Java 應用的啟動時間得以大幅度縮短,這為開發者們帶來了巨大的便利。

然而,我們也意識到,雖然 GraalVM 提供了諸多優勢,但它並非沒有挑戰。相容性問題仍然是一個主要障礙,許多老舊 JDK 專案可能難以遷移到新平臺。此外,某些動態特性在 GraalVM 中的支援仍顯不足,這可能會影響到開發者在使用 Spring 框架時的靈活性與功能實現。尤其是在複雜的企業應用中,這種影響可能更加明顯。

藉助 Spring 框架與 GraalVM 的結合,開發者能夠享受更快的應用啟動速度和更好的資源利用率,但同時也要做好充分的準備,以應對相容性帶來的潛在問題。這意味著,隨著新技術的不斷湧現,我們需要不斷地學習、適應和最佳化自己的開發流程。

image


我是努力的小雨,一名 Java 服務端碼農,潛心研究著 AI 技術的奧秘。我熱愛技術交流與分享,對開源社群充滿熱情。同時也是一位騰訊雲創作之星、阿里雲專家博主、華為云云享專家、掘金優秀作者。

💡 我將不吝分享我在技術道路上的個人探索與經驗,希望能為你的學習與成長帶來一些啟發與幫助。

🌟 歡迎關注努力的小雨!🌟

相關文章