舒服,給Spring貢獻一波源碼。


你好呀,我是歪歪。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM