發現Spring事務的一個實錘bug,官方還拒不承認?你來評評理...


你好呀,我是歪歪。

事情是這樣的,上周我正在全神貫注的摸魚,然后有個小伙伴給我發來微信消息,提出了自己關於事務的一個疑問,並配上兩段代碼:

先說結論:我認為這是 Spring 事務的一個 bug。但是官方說這只能算是文檔上的缺陷,不能算是代碼的 bug。

(好吧,我這篇文章寫了好幾天,所以我寫到上面這一句的時候,官方還不承認是 bug,但是寫完之后他們也承認確實是代碼缺陷。不影響,接着往下看。)

好家伙,我懂了,一切解釋權歸官方所有。

在開始刨根問底之前,我想先就關於如何提問這個問題掰扯幾句。

我把上面這個讀者的問題截出來,是因為我覺得這個提問簡直就是模板方法般的提問。

給了一段示例代碼、給了一段源碼、說明自己的問題、並表明自己已經查詢過但是沒有找到合適的答案。

我讀完他的文字,我很快就能 get 到他的問題是什么,所以我們之間的交流就非常的高效。

最終我們並沒有討論出一個合理的解釋,於是他去提了一個 issues,希望能得到官方的比較權威的回答。

所以我們的故事就圍繞着這個 issues 開始吧。

舞台搭建

在正戲開始之前,我先給你把舞台搭建出來,也就是把 Demo 搞出來。

因為是關於 Spring 事務的問題嘛,所以這個 Demo 主要就是體現出“事務”的應用就行了嘛。

所以 Demo 里面最核心的東西就是這個部分:

其中涉及到的兩個異常就是簡單的自定義異常:

假設這里有一個只允許 10-18 歲的用戶使用奇怪的網站,這個部分就代表這個網站的用戶注冊功能。

接着我們往 user_info 表里面插入一條數據,然后判斷年齡如果大於 18 歲,那么拋出 AgeExceptionOver18 異常,表示這個用戶不是目標用戶。

但是你注意,我 @Transactional 注解里面的 rollbackFor 是 AgeException.class,意思是我並不想回滾這個大於 18 歲的用戶,我還是想把他的注冊信息保存下來,只是拋出一個異常來表示他不是我的目標用戶,

而當我們插入一個年齡小於 10 歲的用戶的時候,會拋出 AgeException.class,應該把剛剛執行的插入語句給回滾掉,我並不想保存這部分用戶的信息。

好的,那么現在就會有小伙伴問了:小於 10 歲的用戶既然不想保存,那么為什么不在插入之前判斷呢?

很好的問題,實際開發中肯定是要在插入之前判斷的,但是我這里只是為了演示事務功能,所以我就這樣寫了,咋地了吧!

上面的代碼,我來搞個接口觸發一下,也就是弄個 Controller 出來:

上面的四個類,就是最關鍵的幾個類,所以我單獨拿出來說一下。

整個項目結構也非常的簡單:

其他的類不關鍵,就不拿出來說了,都是你最拿手的 crud。花五分鍾搭一個這個項目出來不過分吧?中間還能摸兩分鍾的魚。

我把日志級別調整為 debug 級別,接着把項目跑起來驗證一下功能。

然后調用這個鏈接:

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

對應的日志是這樣的:

可以看到我框起來的部分,首先確認執行了 insert 語句,且 age 確實是為 8。但是最后 Rolling back 了,即回滾了。

為什么回滾,我們心里也是門清,因為這里呼應上了:

接下來試一下 age 為 18 歲的用戶:

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

對應的日志是這樣的:

這沒啥說的,事務成功提交,數據插入成功。是我們預期的結果。

數據庫數據也沒毛病:

然后試一下 age 為 28 歲的用戶。

這個用戶我們的預期是拋出 AgeExceptionOver18 異常,但是數據得插入成功。

來走一個:

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

對應的日志是這樣的:

首先數據居然回滾了???

異常倒是拋出來了,但是這也沒呼應上啊!

先不管到底啥原理吧,從我的認知來說,首先我的 @Transactional 注解用法絕對沒有錯,事務配置沒有絕對沒有錯,我的異常也沒有亂拋,你憑什么給我回滾了?

你還說這不是 bug?

just 改改 documentation 就行了?

言外之意是要“抵賴”,強行從文檔上找補嗎?

這不是欺負老實人嗎?

額...等等,我寫這段的時候情況是這樣的。

但是等我寫完這段,第二天再次進 issues 里面去看,發現事情發生了變化,官方又承認這是一個 bug 了,會在 5.3.x 版本里面修改文檔上的描述,會在 6.0 版本里面進行代碼上的修復。

但是我前面已經鋪墊了這么多,已經寫好了,我就不改了,就當在這里留下一個創作痕跡吧,哈哈。

我們接着往下看。

戲劇沖突

一部戲,肯定有它的戲劇沖突,這是它的核心部分。那么我們 Demo 里面的核心沖突是什么呢?

這一小節就先告訴你“戲劇沖突”在哪。

我先問你一個問題:

Spring 管理的事務,默認回滾的異常是什么呢?

我們帶着這個問題去看源碼,找到了這個問題的答案,你就能絲滑入戲。

先搞個斷點,把程序跑起來,然后看調用棧:

可以看到調用棧里面和事務相關的有這樣一個方法:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

這就是我們的突破口。

什么,你問我怎么一下就找到了這里來的?

我只能說:熟能生巧而已。

好吧,其實是有技巧的,你可以自己試着去找一下,因為這不是本文重點,所以我就不多說了。

方法執行異常之后,會走到 catch 代碼塊里面,下面這一行代碼就是異常相關處理的入口:

在我們 age=28 的這個場景下,這個方法進來之后,首先 ex 參數就是我們自定義的 AgeExceptionOver18 異常:

我還框起來了一行代碼:

txInfo.transactionAttribute.rollbackOn(ex)

這一行代碼你看名字 rollbackOn 也知道是判斷 ex 參數是否匹配指定的回滾異常。

如果匹配呢?如果不匹配呢?

如果匹配,rollback。

如果不匹配,commit。

好了,我們接着往下看。

你會走到這里來:

org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn

而這個方法,當 winner 為 null 的時候,上面有個注釋,是說沒有匹配到對應的規則。

也就是我們什么都不配置,默認情況下,winner 就是 null。

那么下面這行代碼里面就藏着我們要找的問題的答案:

return super.rollbackOn(ex);

所以 Spring 管理的事務,默認回滾的異常是什么呢?

源碼告訴我:如果當前拋出的異常屬於 RuntimeException 或者 Error 都會回滾。

前面都是我在鋪路,只是為了把你引到 rollbackOn 方法這個地方來,甚至 super.rollbackOn(ex) 這行代碼都是煙霧彈,本文中我們完全不必關注。

我們需要關注的,是這個部分:

首先我們明確一下這個 rollbackRules 是啥玩意。

在我們的 Demo 里面,它就是我們配置在 @Transactional 注解上 rollbackFor 屬性中的 AgeException.class 對象的全路徑名稱:

好,接下來就關鍵了,你一定要打起精神來。

重點關注這一行代碼:

int depth = rule.getDepth(ex);

來,看一下 rule 和 ex 分別是什么東西:

rule 里面的 exceptionName 是我們配置的 AgeException 對象的全路徑名稱。

ex 是我們程序拋出的 AgeExceptionOver18 異常。

這場戲的核心沖突,就藏在這里的這個方法里面:

org.springframework.transaction.interceptor.RollbackRuleAttribute#getDepth(java.lang.Throwable)

好,至此,舞台搭建完成,核心沖突已經暴露出來。

好戲准備開場。

大幕拉開

你仔細看我框起來的代碼。

前面的 exceptionClass.getName() 是啥玩意?

它長這樣:

后面的 this.exceptionName 是啥玩意?

它是這個玩意:

接下來,神奇的事情就要發生了,鐵子。

com.example.transactional.exception.AgeExceptionOver18
com.example.transactional.exception.AgeException

雖然這是兩個不同的異常,但是這兩個字符串進行 contains 操作,你說是不是返回 true?

所以,這里的 depth 會返回 0。

那么這里的 winner 不會為空

因此這個方法的返回值就會是 true:

還記得我前面說的嗎,這里返回 true 會執行什么代碼?

是不是就 rollback 回滾了?

所以,萬惡之源就是我們大幕拉開的時候就提到的這一段代碼:

org.springframework.transaction.interceptor.RollbackRuleAttribute#getDepth(java.lang.Class<?>, int)

到這里,我覺得已經非常明確了:這難道不是 bug 嗎?你強如 Spring 難道還想狡辯?

但是,如果下面這兩個字符串進行 equals 操作,你說是不是返回 false,問題就得到解決了?

com.example.transactional.exception.AgeExceptionOver18
com.example.transactional.exception.AgeException

道理是這么個道理,但是我覺得問題肯定沒這么簡單。

首先我覺得這里用 contains 肯定是故意的,但是具體出於什么目的,我還真不確定。

於是和我討論的讀者提出一個看法,會不會是為了滿足 rollbackForClassName 這個屬性:

因為當我們用 rollbackForClassName 的時候可以用字符串數組的形式去配置多個需要回滾的異常名稱,比如我搞個 NullPointerException:

在正常使用的場景下,我們是可以完成回滾操作的。

對應地方的代碼的值是這樣的;

java.lang.NullPointerException 字符串當然包含了 NullPointerException 字符串。所以我們進行回滾嘛。沒毛病。

但是如果我們用 equals 操作,那么就匹配不上,導致 rollbackForClassName 屬性失效了。

所以把 contains 修改為 equals 屬於拆西牆,補東牆的措施,不可取。

但是 rollbackForClassName 屬性在我們的 Demo 下,也是沒有效果的。

比如我把程序改成這樣,你說,是不是就亂套了?

同樣的道理嘛。

com.example.transactional.exception.AgeExceptionOver18 字符串當然包含了 AgeException 字符串了。

但是我並不想回滾啊,哥,你好好看看,我拋出來的異常是 AgeExceptionOver18 呀。

到這里,我想問題我應該已經描述的非常清楚了,要是你還是沒明白問題是什么,那你不用往下看了,再看一下“大幕拉開”這一節。

不然后面你很難入戲。

鋪墊一波

為了把真正的問題更好的拋出來,我必須得先把另外一個相關的問題引出來,作為鋪墊。

首先,我們去 Spring 項目的 issues 里面搜一下 getDepth 方法所在的 RollbackRuleAttribute 這個類。

看看有沒有相關的蛛絲馬跡,結果如下:

經過分析,對我有幫助的也只有第一條內容。

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

題目叫做:

Improve javadoc in RollbackRuleAttribute regarding nested classes。
改進 RollbackRuleAttribute 中關於嵌套類的 javadoc。

從題目我們知道這是一次對於文檔的改進。

那么具體是啥改進呢?

可以看到他的描述中也提到了我們前面分析的那一個“萬惡之源”的方法。

關於他具體說了什么,其實我也不用給你翻譯,直接給你看他提交的代碼就一目了然了:

他主要說個了內部類的問題,而且他這個問題和我們的還有點不一樣。

他的兩個異常類,一個叫 EnclosingException,另一個叫做 EnclosedException,這兩個字符串是不存在 contains 關系的。

那么在內部類的場景下,問題是什么呢?

我也給你演示一個,你只需要看一眼就明白了,示例代碼如下:

需要注意的是,我現在的兩個異常是 AgeException 和 AgeOver18Exception,這二者並不存在包含關系。

前面做 Demo 的時候是 AgeExceptionOver18。

AgeOver18Exception
AgeExceptionOver18

別看花眼了。

你看,內部類的時候拋出異常是這樣的:

throw new AgeException.AgeOver18Exception();

你要是沒回過味兒來,沒關系,斷點一打,代碼一跑就恍然大悟:

看明白沒,鐵子。內部類拋出的異常的全路徑名稱是這樣的:

xxx.UserInfoService\$ AgeException$AgeOver18Exception

這不就包含 AgeException 了嗎,不就匹配上了嗎,不就回滾了嗎?

所以,雖然他這個問題的觸發方式和我前面提到的還不一樣,但是“萬惡之源”是一樣的。

那么解決方案是什么呢?

僅僅是修改了一下文檔,從文檔的角度表明了這個情況是會被回滾的:

對應到源碼,也就是這個地方的注釋:

org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(java.lang.Class<?>)

好,我們現在冷靜的思考一下,這里僅僅是從文檔的角度來修復這個問題,在文檔里面明確說明指定異常的內部類也會被回滾,這個做法對不對?

我認為勉強是可以接受的。

比如,我們知道某個異常類被標記為應該被回滾,那么這個異常類的子類應該被回滾,這是沒問題的。

我認為內部類和子類應該保存同樣的邏輯,畢竟它們之前確實存在代碼上的關聯關系,從這個角度上也說的過去。

畢竟一切解釋權歸官方所有嘛。

到這里你要記住:

  • RollbackRuleAttribute 類已經因為在回滾異常的判斷上使用 contains 爆出過內部類的問題。
  • 這個問題通過修改 javadoc 描述的方式進行了修復,沒有修改任何代碼。
  • 這個解決方案勉強說的過去。

好了,鋪墊完成了。

好戲上演

我再次在 issues 里面搜索 RollbackRuleAttribute,會發現多了一條內容:

前面其實我刻意把這一條內容給隱藏了,因為這個 issues 就是和我聊天的讀者提的。

這里的示例代碼就是文章最開頭出現的代碼。

好戲就藏在這個 issues 里面的,一起看一下官方是怎么“反復橫跳”。

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

首先,是一個叫做 snicoll 的哥們把這個 issues 的標題改了一下:

去掉了開頭的“Bug”,這也很正常,屬於提問不標准,現階段只是提問者認為是一個 bug,你這樣取名字,個人主觀意識太過強烈,這樣不好。

主要是你知道修改題目的 snicoll 是誰嗎?

別問,問就是大佬,Spring 和 Spring Boot項目核心維護人員。

然后隔了幾天,這個問題的標題又被另外一個大佬,簡單的修改了一下:

僅僅是把 use contains 修改為了 uses contains(),把 equals 修改為了 equals()

這個小細節完美的體現了 Spring 框架的嚴謹之處,可以說是非常的嚴謹了。

還沒進入到問題解答環節,先把問題的“錯別字”給修改了。

接着就進入了官方答疑環節。

說了下面這么大一堆內容,但是根本不要慌,你知道的我的 English level 是非常的 high 的。這一堆內容分為三大部分,我會一點點的給你說明白:

首先是第一部分:

一上來就是個英語長句,但是根本不要怕。

你看他先是簡明扼要的提到了一個短語“by design”,也就是“設計如此”。

整個翻譯過來大概就是這樣的。

這個地方我們要用 contains() 方法呢?這其實是經過考慮的。

那么是基於什么考慮呢?

在 XML 配置文件中,用戶通常指定自定義異常類型的簡單名稱,而不是全路徑類名。

啥意思呢?

他給了一個文檔中的 xml 配置示例:

那我現在基於我們的 Demo 也搞一個 xml 配置嘛,回到若干年前的基於 xml 配置的方式:

這里我也不得不感慨一句:以前基於 xml 開發的時候是真的麻煩,每次都要去系統項目里面拷一份配置出來,所以我還是很感謝 SpringBoot 的出現的。

這里他想表達一個什么意思呢?

在我的 xml 配置中,關於 rollback-for 屬性。他提到的 simple name 就是 AgeException。而 fully qualified class name 就是 com.example.transactional.exception.AgeException。

就是說這里是不限制用戶填什么的。

如果用戶填的是 simple name,我們也應該讓其生效,所以必須要使用 contains() 方法。

以我理解,這個地方和 @Transactional 注解里面的 rollbackForClassName 屬性的用法是一樣,而這是一個歷史遺留問題,是當年一個不好的設計。

但是我認為不能說考慮不周,畢竟別人也很難想到你會按照那么奇怪的方式去命名異常類啊!

總之這一段話他解釋了為什么會用 contains() 方法,為什么不能用 equals() 方法。

和我們前面分析的基本一致,只是我們沒有想到 XML 的配置方式。

第二段,他開始從文檔的角度來解釋這個問題。

叫我們關注一下 RollbackRuleAttribute 上的 Javadoc 描述。

這里有一個“NB”,不是我們常常說的牛逼,而是一個縮寫:

你看,又在我這里學到一個用不上的英文知識。

我們接着看,主要關注我划線的兩句。

第一句是說:由於使用的 contains() 方法,“Exception” 幾乎可以匹配任何規則,並且可能會隱藏其他規則。但是“java.lang.Exception”這個全路徑的字符串,那么匹配范圍就小了很多了。

第二句是說:由於使用的 contains() 方法,對於 “BaseBusinessException” 等不尋常的異常名稱,不需要使用類的全路徑名稱。

所以,第二段他想表達的是:文檔上我們已經說過了,對於匹配規則,要仔細思考,要非常的小心:

u1s1,他確實寫了,但是你覺得你會看嗎?

第三段就很簡單了:

看到 its subclasses, and its nested classes. 就知道這是我們前面“鋪墊一波”小節說過的部分。

所以你現在知道我為什么給你鋪墊了吧?

如果不給你鋪墊一波,你突然看到一個內部類的單詞 nested classes,你說你一下反應得過來嗎?

你要永遠相信我的行文結構。

好了,現在看另外一句我標注的地方,翻譯過來是說:在當前的實現中最后一句話並沒有遵守。

這里的“最后一句話”就是指 RollbackRuleAttribute 的 Javadoc 的最后一句,也就是 ... its subclasses, and its nested classes 這句。

當然沒遵守了。

我寫的 Demo 里面的兩個異常即不存在子類父類的關系,也不存在內部類的關系。

所以我覺得很納悶:這個 Javadoc 和我的問題之間並不存在關系,或者說並不沖突啊。前面我也說了,關於這部分的 Javadoc 我覺得是沒有毛病的。如果你想要從修改文檔的角度來解決這個問題,也不應扯到子類,內部類啥的,應該是完全另起一行才對。

但是具體怎么解決,他並沒有立即表態,而是把這個 issues 放到了 Triage Queue 里面:

Queue,隊列,你肯定都知道。

Triage 是個啥?

我也不知道,於是我也學到了一個新單詞:

也就是說官方把這個 issues 放到了“待分類的”一個隊列里面,說明他目前是了解到了問題的所在,但是具體應該怎么解決,還沒有定論,有待商榷。

隔了一天這個老哥又來表態了,開始“橫跳”:

他說他又想了一下,需要更正他之前的說法:RollbackRuleAttribute(Class) 構造函數的 Javadoc 是 mostly correct,也就是基本沒毛病的。需要改進的是關於回滾規則上的描述。

總之他還是想從文檔的角度來修復這個問題。

但是解釋了我前面的疑惑:即使從修改文檔的角度來解決這個問題,也不應扯到子類,內部類啥的,應該是完全另起一行才對。

他這里的“回滾規則”也就是“另起一行”。

接着,他對任務的狀態進行了流轉:

從“待分類”移動到了“文檔”的標簽下。

然后表示在 5.3.17 這個里程碑版本中會進行修復:

同時,再次修改了 issues 的標題:

Transaction rollback rules may result in unintentional matches for similarly named exceptions
事務回滾規則可能會導致無意中匹配到名稱相似的例外情況

其實如果讓我來處理這個問題,我大概率也是會從文檔的角度入手,並且最多加一點提醒日志,畢竟這是你使用不規范導致的。

而且我文檔上已經說明有“坑”了,你自己沒看踩進去了,這怪不得我呀。

但是在和這個讀者表達了我的觀點之后,他提出的不一樣的看法:

他覺得使用者大多並不關注日志,主張拋出異常的方式進行強提醒:

於是他在 issues 上表達了自己的看法:

他覺得需要更精准的匹配規則,大多數人是不看文檔的。

接着官方采納了他的意見,並把該需求移動到了 6.0.0-M3 這個里程碑的版本中去實現:

他的具體回復如下:

他說:老鐵,我同意你關於“需要更精准的匹配規則”的觀點。

我們會修復 5.3.x 的文檔描述。

然后在 6.0 版本中,我們會改進一版代碼。

具體來說是這樣的:

  • 如果異常模式是以字符串形式提供的。例如,在 XML 配置中或通過 @Transactional(rollbackForClassName = "example.CustomException") 配置,那么現有的 contains() 邏輯將繼續被使用。
  • 如果一個具體的異常類型是以類引用的形式提供的。例如,通過 @Transactional(rollbackFor = example.CustomException.class),將會有新的邏輯。它完全采用配置上提供的類型信息,從而避免了在 example.CustomException(沒有2)被提供為異常類型時,與 example.CustomException2 的意外匹配。

他這里提到的 CustomException 和 CustomException2,其實是他的測試用例里面的代碼。類比於我們前面的 AgeException 和 AgeExceptionOver18 這兩個異常。

接着,他對這個 issues 進行了重新分類,從“文檔”類型,修改為了“enhancement”類型:

enhancement,是個四級詞語,背一下,會考:

表示這個 issues 需要通過修改代碼來使健壯性更強。

然后再次修改了標題:

對於事務回滾規則,應該使用異常的類型信息,而不是用模式匹配。

本來故事到這里都已經是大結局了,我寫到這里的時候就准備收尾了。

想着收尾不着急,先睡一覺再說。

結果...

第二天早上起來,他!又!更!新!了!

我還得補一段內容。

最后一集

早上起來,我一刷新頁面,發現官方針對這個 issues 進行了最后一次提交:

這次 issues 的標題,最后定格為:

Support type-safe transaction rollback rules
支持類型安全的事務回滾規則

而這次對應的代碼提交鏈接是這樣的:

https://github.com/spring-projects/spring-framework/commit/c1033dbfb3609f3b3fe002d7b582b3302620c05a

里面寫了很長一段的內容,來描述這次提交的背景,但是基本上都是我前面寫過的東西的總結:

結合我前面寫的東西,我給你翻譯翻譯:

首先我覺得是在事務模塊里面創造一個新的概念:type-safe rollback rules,類型安全的回滾規則。

在這次提交之前,只有一種事務回滾機制, Pattern-based rollback rules,即基於匹配模式的回滾規則。

而官方說基於匹配模式的回滾規則,會帶來三種意料之外的匹配情況:

  • 不同包中的相同命名的異常類,會被意外匹配上。比如,example.client.WebException 和 example.server.WebException 都會與 “WebException” 模式匹配。
  • 在同一個包中有類似命名的異常,這里說的相似是指當一個給定的異常名稱是以另一個異常的名稱開頭時。例如:example.BusinessException 和 example.BusinessExceptionWithDetails 都與 “example.BusinessException”模式匹配。
  • 嵌套異常,也就是當一個異常類被聲明在另一個異常類里面的時候。例如:example.BusinessException 和 example.BusinessException$NestedException 都會與 “example.BusinessException” 匹配上。

第一種沒啥說的,請使用全路徑名稱去避免。

第二種就是我們文章中的例子,需要通過修改代碼解決。

第三種內部類的情況我也在前面鋪墊過了。但是當時的解決方案是僅增加文檔中對應的描述。

但是現在,你看他怎么說的:

這次的提交可以防止后兩種情況的意料之外的匹配。也就是說這次提交不僅修復了我們的問題,還修復了內部類的問題。

那么怎么修復的呢?

首先是在 RollbackRuleAttribute 類里面新增了一個 exceptionType 字段:

然后在構造方法里面對其進行賦值:

核心代碼變成了這樣:

當 exceptionType 字段,即全路徑名稱不為空的時候,使用 equals() 方法,也就是type-safe rollback rules。否則使用 contains() 方法,也就是 Pattern-based rollback rules。

另外,關於 Javadoc 上的很多描述也發生了變化,我就不一一舉例了,強烈建議你自己去看看這次提交。

我只特別拿出一處變化來給你看看:

去掉了“內部類”,改成了“類型安全”。

至此,問題得到解決。

但是在 XML 里面或者用 @Transactional 注解里面的 rollbackForClassName 屬性,也就是使用匹配模式的時候,還是會有意料之外的匹配情況。

官方:反正我在文檔上說清楚了,你要是還踩坑,那就怪得不我了?

最后,再插一個關於編程規范的事兒。

你想這次這個問題完全是因為你有兩個這樣的異常類名稱引起的:

AgeException
AgeExceptionOver18

而對於異常類,我們都約定成俗的要求必須以“Exception”結尾。

包括阿里巴巴 Java 開發手冊在命名風格里面也特意提到了這一點,且是強制要求:

所以,如果我們都遵守這個規則,大家就相安無事。

那么,這個故事最后告訴我們一個什么道理呢?

它告訴我們...

它告訴我們規則就是拿來打破的,如果你不打破規則,永遠也踩不到這個坑,也就不會推動 Spring 的改動。

打破規則,這是你的一小步,卻是開源世界的一大步。

所以,兄弟們,鐵子們,不要墨守成規,要打破...

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


免責聲明!

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



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