Spring Framework之再探Core Container(中)

特別說明

這是一個由simviso團隊進行的關於Spring Framework 5.2版本內容分享的翻譯文檔,分享者是Spring Framework 5.2項目leader。
視頻第一集:https://www.bilibili.com/vide...

視頻第二集:https://www.bilibili.com/vide...

視頻翻譯文字版權歸 simviso所有,未經授權,請勿轉載
圖片描述
參與人員名單:
圖片描述
順帶推薦一個專業的程序員後端微信羣的圈子:
圖片描述

1. GenericApplicationContext

出於多種目的,特別是在GenericApplicationContext這裏,我們專門提供了Kotlin擴展。你不需要特意引入它們(Kotlin支持),不需要額外的步驟,它們已經成爲Spring Framework核心中的一部分。so,無論你使用的是 Spring context 5.0、5.1、5. 2 中任何版本,你都會自動獲得帶有 Kotlin 擴展的 GenericApplicationContext。如果你選擇使用 Kotlin 來進行開發,那麼它們就會被 Kotlin 編譯器檢測編譯。
圖片描述
來看這個GenericApplicationContext,它的處理方式是不是看起來很熟悉,但它是使用Kotlin來寫的。可以看到,圖中下面的和上面的版本明顯有一些區別。舉個例子來講,我們撇開Bar.class,然後來說這裏該如何去創建一個bar實例。我們只需要擁有一個Supplier實例即可,so,我們來看registerBean僅僅需要一個基於構造器調用的supplier實例,沒有其他。

理由其實很簡單,因爲這並不是Java裏的Lambda表達式,而是一個Kotlin函數。它實際上調用了一個由Kotlin實現的registerBean重載函數。在我們的 Kotlin 擴展中,Kotlin擴展函數是基於一個元數據模型(T::class.java)
一個反射模型(BeanDefinitionCustomizer)進行設計的。當我們來問一個Kotlin函數,你會返回什麼,它預先已經知道了,而此時Java 8 lambda表達式是無法進行返回類型檢查的。基於此,我們可以很好去使用這個特性,僅需要一個supply實例(注:無須使用T.class進行類型限定)就可以知道所得到的組件類型。即通過suplier 實例創建的bean的類型。

我們可以通過Kotlin中一些其他函數API來提高開發體驗。下面的這個版本基本上只是使用了一點語法糖,有一點點的語法差異。這是另外一種應用Kotlin語言特性來實現目標API的方式。這不是一個正式的Gradle風格,只是有一點DSL(領域專用語言)的風格。感覺有一點像Gradle構建工具 ,Kotlin風格的Gradle構建工具。

so,這裏通過一種不一樣的風格來表達Generic(這裏指GenericApplicationContext),但這只是Kotlin語言的一種變體,我們這裏用它來實現我們的目的,沒有什麼特別的。

2. 性能調優與GraalVM

圖片描述
ok,讓我們開始轉向另一個重要的話題。對於Spring Framework 5,我們一直致力於不斷調整並提高它的性能

當然我們也在努力改善提高開箱即用的性能。雖然和我們的目標相關,但努力的方向有點不一樣。我們試圖在代碼庫中避免一些性能很差的東西來減少不必要的開銷。同時我們也有嘗試爲你提供帶hook的設施,一種你可以用來調整性能的機制。如果你知道的話,你就可以根據它做出最具體的假設,這些假設無法通過通用的框架代碼來實現。
圖片描述
在5.2中最明顯的例子就是註解處理。最初,在5.1版本中,我們主要通過對現有代碼進行修訂,但在5.2中我們選擇完全重新實現。遺憾的是,在java中註解的處理是一個相當複雜的事情。如果你之前有做過這樣的事情,你也許就知道我的意思,這也是框架日常所做的絕大多數事情。在一個應用程序的代碼中,你幾乎不需要去寫如何查找一個註解。也就是說你只需要聲明註解,框架會負責對它們進行正確的查找。這個過程其實非常複雜,效率又很低。主要原因在於它們是由Java來實現的。我們爲了做到最好,在這個方面我們儘量避免反射,避免通過代理。然而不幸的是,註解實例是通過Java代理實現的,主要也是基於jdk自身。因此,我們竭盡所能,從開始就避免使用反射。

關於AnnotationUtils和AnnotatedElementUtils 這兩個API基本上是相同的,你在使用SpringBoot的過程中自然會使用到它們(我們會使用註解,那就會使用到這些工具類)。你很少會親自去使用它們,但是基本上它們對你來講是透明的。(也就是你看不到,你只需要關心使用什麼註解,不需要關心註解背後的實現)如果你使用了其他的Spring項目例如Spring Integration,Spring Batch,那你就可以從這個透明特性裏面得到很明顯的好處。

在Spring 5.2中有一個名爲MergedAnnotations的API,它對Spring聲明過的註解層次結構的內省非常有用。你可能感覺到Spring的註解模型很複雜了。這裏有個元數據註解模型,你可以在此之上覆寫它的屬性。你不需要做很複雜的事情,你可以很輕易地通過這些選項獲取到。因此我們引入了一種全新的API,它可以使所有的內部檢查變得非常簡單直接,並且十分高效。

我們的精力更多放在了對於組件中可存在註解和不可存在註解的註冊上面。你可以通過編程規範來告訴容器某些註解類型只能存在於特定的組件類裏面。換句話講,在這種特定的組件類和特定的組件包中,根本就不可能有這些註解類型的使用。我們不需要在這些地方去查找這些註解。在這些地方,你根本就找不到它們的。

這些假設我們很難在程序中自己通過代碼來實現。正常情況下,註解可以被應用於任何地方,這是一個常識。可能由於一些規定,你需要在你或團隊的代碼庫中服從一些約定,即特定的地方只能用一些特定的註解類型。如果你告訴我們這些約定,我們就可以在運行代碼時減少這些註解產生的性能開銷。對此我們已經在5.2中做了大量的工作,也就是通過這些信息在註解查找的時候儘量跳過它。以此來整合出一個Java 標準的索引排列。

我們在啓動的時候並沒有索引,我們只有這些類文件。在運行時類索引並不指向類文件(指向的是JVM裏面的class字節碼)。我們只能通過兩種途徑對註解進行內省。如果我們在構建時想要獲取額外信息的話,就可以通過像Jandex的索引或者是一個自定義的索引排列一樣來達到目的(通過索引來 存儲一些關鍵信息)。就好像你在其他的基礎架構中所使用的索引一樣。在啓動時如果通過加載這樣的一個索引來提取信息,這個信息可能是Spring ApplicationContext相關的內容,通過這個索引我們就能立馬獲取到這個信息。這些在我們的BootStrap代碼中都有提供配置可進行調整。我們也會在SpringBoot中會對引用排列進行重新評估,尤其是這個東西它是不是已經可以被SpringBoot自動使用。如果在使用時,發現了一個Jandex索引,SpringBoot會自動識別評估並應用它。

關於這塊,接下來的路還很長,但在七月份我們會將我們這些想法放到SpringBoot中。最重要的是如果你使用了這些功能(索引排列支持),那麼它將會是你整個架構的一個熱點(很明顯會大量的用到,因爲解決了很多痛點)。同時,你可以對這些可用功能進行調整以避免不必要的開銷。
圖片描述
今天我們另一個主題則是GraalVM,它是最近比較火的一個話題。Spring框架對它的支持已經有一段時間了(即將Spring Boot Application封裝成一個GraalVM Native Image在上面運行),現在這部分仍然處於實驗階段。目前,我們在github上已經簡單創建了一個基於GraalVM 19GA版本的wiki。通過它,人們可以進行有針對性的討論,今年晚些時候,我們也會對其進行專門的討論。
圖片描述
GraalVM Native Image 它到底是什麼東西?它是一個很特別的部署架構。它並不是我們常見的JVM,兩者完全不一樣。它沒有任何動態類加載,也沒有任何動態內省。很直接的講,它並沒有你想的那麼與衆不同。GraalVM同樣支持反射。你只需要給Native Image工具提供配置文件,這樣它就能預先將正確的信息內置進Native Image。所以,在運行時你不能做任何動態反射,但是你可以將你需要用到反射的地方進行提前配置。在這個基於GraalVM的定製版的Spring Framework application中我們對prototypes 進行了實驗,得到了比索引排列更好的效果。

拿我們之前很熟悉的函數式Bean Registration(前面ppt中的例子)。通過內聯的Supplier註冊Bean的過程可以很自然地在GraalVM上運行,你不需要去做什麼。在這裏我們需要討論的是在GraalVM中,基於註解的組件模型需要進行一些額外的工作,你需要提前告訴GraalVM中的Native Image,你所要操作的組件類型以及內省。

我們當下的目標基本還是爲GraalVM做準備,我們已經避免了一些不必要的反射點,同時也重製了一些代碼,例如可以自動跳過那些沒用的以及對GraalVM沒有任何意義的工作,以提升開箱即用的體驗。它們其實已經在5.1中出現了不少,在5.2中尤甚。

附帶說明(wiki文檔)裏面也提及如何使用GraalVM 19早期採用版本中的Native Image工具。在我們Spring Framework 5.3下一次迭代中,主要目標在於提升開箱即用的性能體驗。通過整合寫開箱即用的配置和構建工具,
你可以很輕易的構建一個用於部署的基於Spring的Application GraalVM Native Image。目前而言,任重道遠。毋庸置疑,我們現在也不清楚這個工具未來會是怎樣的,但我們已經和Oracle團隊在GraalVM上緊密合作了相當長的一段時間。自從基於Spring 的 Native Image可以在GraalVM上進行部署,兩者結合的優點也已經體現出來了。爲了可以在GraalVM上運行,我們已經做了相當一些改進。在對 GraalVM 19 使用時我們給出了反饋意見,之後我們會提供更多的反饋,以期望這些反饋會體現在GraalVM 20上。

從前面所講的這兩種方式來看,我們可以發現具有開箱即用功能的索引排列在當下可能是更優的選擇。這個是我們當前維護的wiki頁面。這基本上也是目前現有的狀態,我也在wiki上列出了我們的一些前進方向。

當然,我們選擇將基於Spring的應用程序通過Native Image的方式進行部署的主要理由,就是Native Image基於一種完全不同的內存消耗模型以及更加快速的啓動方式。通過使用Native Image,我們能獲取大量好處,同時也得進行一些取捨。你會犧牲一些性能,即在構建的的時候,你需要在生成Native Image上花很多時間。對我來說,這不是個簡單的決定。如果你真的需要性能上的調優,從我的觀點而言,你可以選擇我們所提供的首選方案(即索引排列)。
圖片描述
接下來有一些建議。如果你爲了性能提升去選擇優化特定的應用或者架構組織,你就必須在你的代碼或你的組件模型結構上做出一定的妥協。在此我們提供了一系列的透明特性來讓你們獲得好處,同時將它們做的儘可能透明。當然你需要去找到你所要用到的配置選項進行配置。例如,如果你使用代碼的形式去註冊,這樣會顯得很臃腫。但如果你能明確聲明哪些組件你不需要,那麼在啓動的過程中就能減少這些組件類的加載,以此來提高性能。這需要你進行很多的微調。相應的在Spring 5.2中,我們提供了一些特定的設施。例如一些配置類,你可以選擇將proxyBeanMethods設定爲false來避免在運行時創建CGLIB的子類。如果你的配置類不會出現一個Bean方法調用另一個Bean方法,否則導致它們會被重定向到容器(會通過代理類進行調用)。如果你的Bean彼此獨立,不會互相調用,那麼你可以在一個特定的配置類裏面設置proxyBeanMethods爲false來避免在運行時生成代理子類。我們已經將上面的功能整合進了Spring Framework5.2和Spring Boot 2中。

另一個有點奇怪的建議就是,我想我更喜歡基於接口的代理。每個人都習慣使用代理來映射到目標類,也就意味着在運行時會創建CGLIB的代理子類。但是實際上基於良好的舊的接口代理,在啓動時創建的效率更高,並且它們可以在GraalVM上做到開箱即用。Spring總是在基於接口的代理和基於類的代理之間進行默認使用選擇(對Spring Boot同樣如此)。你可以使用任何一種AOP的對應實現,你也可以明確的指出你想要使用的代理類型。如果你的組件模型結構是使用接口代理來實現的,那麼這將會對性能產生一個極大的提升。這對轉向GraalVM 有特別的意義,因爲在這裏我們不需要考慮CGLIB的配置。

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