你好呀,我是歪歪。
這周我在 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技術],歡迎關注,第一時間接收最新文章。