舒服,給Spring貢獻一波原始碼。

why技術發表於2022-03-14

你好呀,我是歪歪。

這周我在 Spring 的 github 上閒逛的時候,一個 issues 引起了我的興趣。

這篇文章,是我順著這個 issues 往下寫,始於它,但是不止於它:

https://github.com/spring-projects/spring-framework/pull/27818

這個 issues 標題翻譯過來,就是說希望 @Async 這個註解能夠支援佔位符或 SpEL 表示式。

而我關注到這個 issues 的原因,完全是因為我之前寫過 @Async 相關的文章,看著眼熟,就隨手點進來看了一下。

在這個問題裡面,提到了一個編號為 27775 的 issues:

https://github.com/spring-projects/spring-framework/issues/27775

這個說的是個啥事兒呢?

估計你看一眼我截圖中標註的地方也就看出來了,他想把執行緒池的名稱放到配置檔案裡面去。而這個需求我覺得並不奇怪,基於 Spring 框架來說,是一個很合理的需求。

搞個 Demo

我還是先給你搞個 Demo,驗收一下它想要幹啥。

首先注入了一個名稱為 why 的執行緒池。

然後有一個被 @Async 註解修飾的方法,而這個註解指定了一個值為 why 的 value,表明要使用名稱為 why 的這個執行緒池:

接著我們還需要一個 Controller,觸發一下:

最後在啟動類上加上 @EnableAsync 註解,把專案啟動起來。

呼叫下面的連結,發起呼叫:

http://127.0.0.1:8085/insertUser?age=18

輸出結果如下:

說明配置生效了。

然後,提出 issues 的這個哥們,他想要這麼一個功能:

也就是讓 @Async 註解和配置檔案進行聯動。

目前 Spring 的版本是不支援這個東西的,比如我把專案啟動起來之觸發一次:

直接丟擲了 NoSuchBeanDefinitionException,說明 @Async 的 value 註解並沒有解析表示式的功能。

支援一波

好的,現在需求就很明確了:目前不支援,有人在社群提出該需求,想要 Spring 支援該功能。

然後這個叫 sbrannen 的哥們出來了:

他說了兩句話:

  • 1.如果提供的 BeanFactory 是 ConfigurableBeanFactory,我們似乎可以通過修改 org.springframework.aop.interceptor.AsyncExecutionAspectSupport.findQualifiedExecutor(BeanFactory,String) 的程式碼,使用 EmbeddedValueResolver 來支援。
  • 可以看一下 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.setBeanFactory(BeanFactory),這是一個對應的例子。

第一句話中,他提到的 findQualifiedExecutor 方法,也就是需要修改的地方的程式碼,在我的 5.3.16 版本中是這樣的:

你先記住入參中有一個 beanFactory 就行了。

而第二句話中提到的 setBeanFactory 方法,是這樣的:

他說的 “for an example” 就是我框起來的部分。

這裡面關鍵的地方有兩個:

  • ConfigurableBeanFactory
  • EmbeddedValueResolver

首先 ConfigurableBeanFactory ,在 Spring 裡面是一個非常重要的類,但是不是本文重點,一句話帶過:你可以把它理解為是一個巨大的、功能齊全的工廠介面。

重點是 EmbeddedValueResolver 這個東西:

從註解上可以知道這個類是用來解析佔位符和表示式。相當於是 Spring 給你封裝好的一個工具類吧。

EmbeddedValueResolver 裡面就這一個方法:

而這個方法裡面呼叫了一個 resolveEmbeddedValue 方法:

org.springframework.beans.factory.support.AbstractBeanFactory#resolveEmbeddedValue

這個方法就是 Spring 裡面解析表示式的核心程式碼。

我給你演示一下。

首先我們加一點程式碼:

這個程式碼不需要解釋吧,已經很清晰了。

我只需要在我們前面分析的程式碼這裡打上斷點,然後把程式跑起來:

是不是很清晰了。

入參是 ${user.age} 表示式,出參是配置檔案中對應的 18。

關於如何解析的所有祕密都藏在這一行程式碼裡面:

你以為我要給你詳細講解嗎?

不可能的,指個路而已,自己看去吧。

現在我要開始拐彎了,拐回到這個老哥的回覆上:

現在我先帶你捋一捋啊。

首先,有個老鐵說:你這個 Spring 的 @Async 註解能不能支援表示式呀,比如這樣式兒的 @Async("${thread-pool.name}")

然後官方出來回覆說:沒問題啊,我們可以修改 findQualifiedExecutor 方法,在裡面使用 EmbeddedValueResolver 這個工具類來支援。比如就像是下面這個類中的 setBeanFactory 方法一樣:

接著我帶你去看了一下這個方法,然後知道了 EmbeddedValueResolver 的用法。

好的,那麼現在問題來了:在 findQualifiedExecutor 方法中,我們怎麼使用呢?

兜兜轉轉一大圈,現在就回到最開始的那個 issues 裡面:

這個老哥說他基於 sbrannen,也就是官方人員的提示.提交了這次修改。

怎麼修改的呢?

看他的 Files changed:

修改了三個檔案,其中一個測試類。

剩下兩個,一個是 @Async 註解:

這裡面只是修改了 Javadoc,表示這個註解支援表示式的方式進行配置。

另外一個是 AsyncExecutionAspectSupport 這個類:

在 findQualifiedExecutor 方法裡面加了五行程式碼,就完成了這個功能。

最後,官方在 review 程式碼的時候,又刪除一行程式碼:

也就是 4 行程式碼,其實應該是 2 行核心程式碼,就完成了讓 @Async 支援表示式的這個需求。

而且官方是先給你說了解決方案是什麼,只要你稍微你跟進一下,發動你的小腦殼思考一下,我想你寫出這 4 行程式碼也不是什麼困難的事情。

這就是給 Spring 貢獻原始碼了,而且是一個比較有價值的貢獻。如果是你抓住了這個機會,你完全可以在簡歷上寫一句:給 Spring 貢獻過原始碼,讓 @Async 註解支援表示式的配置方式。

一般來說對 Spring 瞭解不深入的朋友,看到這句話的時候,只會覺得很牛逼,想著應該是個大佬。

但是實際上,2 行核心程式碼就搞定了。

所以你說給 Spring 貢獻原始碼這個事兒難嗎?

機會總是有的,就看你有沒有上心了。

什麼,你問我有沒有給 Spring 貢獻過原始碼?

我沒有,我就是不上心,咋的了。

這是我寫這個文章想要表達的第個觀點:

給開源專案貢獻原始碼其實不是一件特別困難的事情,不要老想著一次就提交一整個功能上去。一點點改進,都是好的。

除錯技巧

前面提到的程式碼改進, Spring 還沒有釋出官方的包,但是我想要自己試驗一下,怎麼辦呢?

你當然可以把 Spring 的原始碼拉下來,然後自己編譯一波,最後本地改改原始碼試一試。

但是這個過程太過複雜了,基本上可以說是一個勸退的流程。

為了這麼一個小驗證,完全不值當。

所以我教你一個我自己研究出來的“騷”操作。

首先,我本地的 Spring 版本是 5.3.16,對應這部分的原始碼是這樣的:

還是先改造一下程式:

然後把程式跑起來,觸發一次呼叫,就會停在斷點的地方:

這個時候我們可以看到 qualifier 還是一個表示式的形式。

接著騷操作就來了。

你點選這個圖示,對應的快捷鍵是 Alt+F8:

這是 ide 提供的 Evaluate Expression 功能,在這個裡面是可以寫程式碼的。

比如這樣:

它還可以偷樑換柱,我在這裡把 qualifier 修改為 “yyds” 字串:

然後跑過斷點,你可以從異常資訊中看到,它是真的被修改了:

那麼,如果我把這次提交的這 4 行程式碼,利用 Evaluate Expression 功能執行一下,是不是就算是模擬了對應的修改後的功能了?

我就問你:這個方法“騷”不“騷”。

接下來,我們就實操起來。

把這幾行程式碼,填入到 Evaluate 裡面:

if (beanFactory instanceof ConfigurableBeanFactory) {
 EmbeddedValueResolver embeddedValueResolver = new EmbeddedValueResolver((ConfigurableBeanFactory)beanFactory);
 qualifier = embeddedValueResolver.resolveStringValue(qualifier);
}

輸入程式碼片段,記得點選一下這個圖示:

點選執行之後是這樣的:

然後看輸出日誌,你可以看到這樣一行:

說明我的“偷樑換柱”大法成功了。

這不比你去編譯一份 Spring 原始碼來的方便的多?

而且這個除錯的方法,相當於是你在 debug 的時候還能再額外執行一些程式碼,所以有的時候真的有時候能起到奇效。

這是我寫這篇文章的第二個目的,想要分享給你這個除錯方法。

不同之處

細心的讀者肯定發現了,官方的程式碼有點奇怪啊:

首先 instanceof 是 Java 的保留關鍵字,它的作用是測試它左邊的物件是否是它右邊的類的例項,返回 boolean 的資料型別。

但是我記得 instanceof 不是這樣用的呀?這是個什麼騷操作啊?

不慌,先粘出來,放到 ide 裡面看看啥情況:

我們常用的寫法都是標號為 ① 那樣的,當我在我的環境裡面寫出標號為 ② 的程式碼的時候,ide 給我了一個提示:

Patterns in 'instanceof' are not supported at language level '8'

大概意思是說 instanceof 的這個用法在 JDK 8 裡面是不支援的。

看到這個提示的一瞬間,我突然想起了,這個寫法好像是 JDK 某個高階版本之後支援的,很久之前在某個地方瞟到過一眼。

然後我用 “Patterns instanceof” 關鍵詞查了一下,發現果然是 JDK 14 版本之後支援的一個新特性。

https://www.baeldung.com/java-pattern-matching-instanceof

我就直接把文章中的例子拿出來給你說一下。

我們用 instanceof 的時候,基本上都是需要檢查物件的型別的場景,不同的型別對應不同的邏輯。

好,我問你,你使用 instanceof,在型別匹配上了之後,你的下一步操作是什麼?

是不是對物件進行強制型別轉換?

比如這樣的:

在上述程式碼截圖中,我們每種情況要通過 instanceof 判斷 animal 的具體型別,然後強制型別轉換宣告為區域性變數,接著根據具體的型別執行指定的函式。

這有的寫法有很多缺點:

  • 這麼寫非常單調乏味,需要檢測型別然後強制型別轉換。
  • 每個 if 都要出現三次型別名。
  • 型別轉換和變數宣告可讀性很差
  • 重複宣告型別名意味著很容易出錯,可能導致未預料到的執行時錯誤。
  • 每新增一個animal 型別就要修改這裡的函式。

注意我加粗的地方,和原文是一樣的,這波強調和細節是拉滿了的:

為了解決上面提到的部分缺點,Java 14 提供了可以將引數型別檢查和繫結區域性變數型別合併到一起的 instanceof 操作。

就像這樣式兒的:

首先在 if 程式碼塊對 animal 的型別和 Cat 進行匹配。先看 animal 變數是否為 Cat 型別的例項,如果是,強轉為 Cat 型別,並賦值給 cat。

需要注意的是變數名 cat 並不是一個真正存在的變數,只是模式變數的一個宣告而已。你可以理解為固定語法。

變數 cat 和 dog 只有當模式匹配表示式的結果為 true 時才生效和賦值。所以如果你一不小心把變數用在別的地方,直接會提醒你編譯錯誤。

所以你對比一下上面兩個版本的程式碼,肯定是 Java 14 版本的程式碼更簡潔,也更易懂。減少了大量的型別轉換,而且可讀性大大提高。

回到 Spring

你看,本來是看 Spring 的,怎麼突然寫到了 JDK 的新特性了呢?

那必然是我埋下的伏筆啊。

我給你看一個東西:

https://spring.io/blog/2021/09/02/a-java-17-and-jakarta-ee-9-baseline-for-spring-framework-6

官方在去年的 SpringOne 大會上就宣佈了:Spring 6.0 和 Spring Boot 3 這兩大框架的 JDK 基線版本是 17。

也就是說:我們很有可能在 JDK 8 之後,下一個要擁抱的版本是 JDK 17。

而我,作為一個技術愛好者的角度來說:這是好事,得支援,大力支援。

但是,作為一個寫著 CRUD 的 Java 從業者來說:想想升級之後各種相容性問題就頭疼,所以希望這個擁抱不要發生在我短暫的職業生涯中。去讓那幫年輕力壯,剛剛入行的小夥子們去折騰吧。

而當我把視角侷限在這篇文章的角度,電光火石之間,我又想到了一個給 Spring 貢獻原始碼的“騷”操作。

歷史程式碼中這麼多用 instanceof 的地方,我只要在 6.0 分支裡面,把這些地方都換成新特性的寫法,那豈不是一個更簡單的貢獻原始碼的方式?

但是,在提交 issues 之前,一般流程都是要先去查詢一下有沒有類似的提交。

所以在幹這事之前,我還是先冷靜的查詢了一下。

一查,我都笑了...

我都能想到,肯定其他人也能想到,果然有人已經捷足先登了。

比如這裡:

https://github.com/spring-projects/spring-framework/issues?q=instanceof

這次對應提交的程式碼是這樣的:

然後,官方還在裡面小小的吐槽了一波:

簡單來說就是:老哥,這樣的小改進,就還是不要提 issue 了吧。你得整個大的啊,別隻改一個類啊。

我覺得也是,你改你改一個模組也行呀,比如這位老哥,改了 Spring-beans 模組下的 8 個檔案:

這樣才是針對這類改動的正確姿勢。

反正我把路指在這裡了,你要是有興趣,可以去看看 Spring 6.0 的程式碼是不是還有一些沒有改的地方,你去試著提交一把。

這個話題又回到我最開始表達的第一個觀點了:

給開源專案貢獻原始碼其實不是一件特別困難的事情,不要老想著一次就提交一整個功能上去。一點點改進,都是好的。

提交的東西確實是和 Spring 框架關係不大,但是你至少能體驗一下給開源專案做貢獻的流程和感覺吧,而且越大的專案,流程約精細,肯定是能學到東西。

而這個過程中學到的東西,絕對比你提交一個 instanceof 改進大的多,所以你還能說這樣的提交是沒有什麼營養的嘛?

比如我去年的一篇文章中,就提到了 Dubbo 在對響應報文進行解碼的時候有一個沒必要的重複操作,可以刪除一行校驗相關的程式碼。

我沒有去提對應的 pr,但是我寫在了文章中。

有個讀者看到後,當天中午就去提交了,官方也很快入庫了。

去年年底的時候 Dubbo 社群搞了一個回饋活動,就給他送了一個咖啡杯:

意外驚喜,一行程式碼,不僅可以學點知識,還可以免費得個咖啡杯,就問香不香。

昇華一下

好了,回顧一下這篇文章。

我從 @Async 支援表示式作為引子,引到了 instanceof 的新特性,接著又引到了 Spring 6 會以 JDK 17 作為基線版本。

其實我寫這篇文章的時候,腦海中一直在縈繞著一句話:大風起於青萍之末。

instanceof,是青萍之末。

大風就是 JDK 17 作為基線版本。

關於為什麼要用 JDK 17 作為基線版本,其實這是風華正茂的 Java 的一次渡劫。渡劫是否成功,關係著我們每一個從業者。

在雲原生的“喧譁”之下,走在前面的人已經感受到:大風已經吹起來了。

比如周志明博士在一次名為《雲原生時代,Java 的危與機》中說了這樣的一段話:

https://icyfenix.cn/tricks/2020/java-crisis/qcon.html

未來一段時間,是 Java 重要的轉型視窗期,如果作為下一個 LTS 版的 Java 17,能夠成功集 Amber、Portola、Valhalla、Loom 和 Panama 的新能力、新特性於一身,GraalVM 也能給予足夠強力支援的話,那 Java 17 LTS 大概率會是一個里程碑式的版本,帶領著整個 Java 生態從大規模服務端應用,向新的雲原生時代軟體系統轉型。

可能成為比肩當年從面向嵌入式裝置與瀏覽器 Web Applets 的 Java 1,到確立現代 Java 語言方向(Java SE/EE/ME 和 JavaCard)雛形的 Java 2 轉型那樣的里程碑。

但是,如果 Java 不能加速自己的發展步伐,那由強大生態所構建的護城河終究會消耗殆盡,被 Golang、Rust 這樣的新生語言,以及 C、C++、C#、Python 等老對手蠶食掉很大一部分市場份額,以至被迫從“天下第一”程式語言的寶座中退位。

Java 的未來是繼續向前,再攀高峰,還是由盛轉衰,鋒芒挫縮,你我拭目以待。

而我,還只是看到了青萍之末。

最後,文章首發於公眾號[why技術],歡迎關注,第一時間接收最新文章。

相關文章