自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我試圖通過這篇文章,教會你一種閱讀源碼的方式

開發(fā) 前端
閱讀源碼的方式非常的多,這篇文章只是站在我個人的角度介紹閱讀源碼的眾多方式中的一種,滄海一粟,就像是一片樹林里面的一棵樹的樹干上的一葉葉片的葉脈中的一個小分叉而已。

你好呀,我是歪歪。

是的,正如標題描述的這樣,我試圖通過這篇文章,教會你如何閱讀源碼。

事情大概是這樣的,前段時間,我收到了一個讀者發(fā)來的類似于這樣的示例代碼:

圖片

他說他知道這三個案例的回滾情況是這樣的:

  • insertTestNoRollbackFor:不會回滾
  • insertTestRollback:會回滾
  • insertTest:會回滾

他說在沒有執(zhí)行代碼之前,他也知道前兩個為什么一個不會回滾,一個會回滾。因為拋出的異常和 @Transactional 里面的注解呼應上了。

但是第三個到底會不會回滾,沒有執(zhí)行之前,他不知道為什么會回滾。執(zhí)行之后,回滾了,他也不知道為什么回滾了。

我告訴他:源碼之下無秘密。

讓他去看看這部分源碼,理解它的原理,不然這個地方拋出一個其他的異常,又不知道會不會回滾了。

但是他說他完全不會看源碼,找不到下手的角度。

圖片

所以,就這個問題,我打算寫這樣的一篇文章,試圖教會你一種閱讀源碼的方式。讓你找到一個好的切入點,或者說突破口。

但是需要事先說明的是,閱讀源碼的方式非常的多,這篇文章只是站在我個人的角度介紹閱讀源碼的眾多方式中的一種,滄海一粟,就像是一片樹林里面的一棵樹的樹干上的一葉葉片的葉脈中的一個小分叉而已。

對于啃源碼這件事兒,沒有一個所謂的“一招吃遍天下”的秘訣,如果你非要讓我給出一個秘訣的話,那么就只有一句話:

啃源碼的過程,一定是非??菰锏?,特別是啃自己接觸不多的框架源碼的時候,千頭萬緒,也得下手去捋,所以一定要耐得住寂寞才行。

然后,如果你非得讓我再補充一句的話,那么就是:

調試源碼,一定要親!自!動!手!只是去看相關的文章,而沒有自己一步步的去調試源碼,那你相當于看了個寂寞。

親自動手的第一步就是搞個 Demo 出來。用“黑話”來說,這個 Demo 就是你的抓手,有了抓手你才能打出一套理論結合實際的組合拳。抓手多了,就能沉淀出可復用的方法論,最終為自己賦能。

搭建 Demo

所以,第一步肯定是先把 Demo 給搭建起來,項目結構非常的簡單,標準的三層結構:

圖片

主要是一個 Controller,一個 Service,然后搞個本地數(shù)據(jù)庫給接上,就完全夠夠的了:

圖片

Student 對象是從表里面映射過來的,隨便弄了兩個字段,主要是演示用:

圖片

就這么一點代碼,給你十分鐘,你是不是就能搭建好了?中間甚至還能摸幾分鐘魚。

要是只有這么一點東西的、極其簡單的 Demo 你都不想自己親自動手搭一下,然后自己去調試的話,僅僅是通過閱讀文章來肉眼調試,那么我只能說:

在正式開始調試代碼之前,我們還得明確一下調試的目的:想要知道 Spring 的 @Transactional 注解對于異常是否應該回滾的判斷邏輯具體是怎么樣的。

帶著問題去調試源碼,是最容易有收獲的,而且你的問題越具體,收獲越快。你的問題越籠統(tǒng),就越容易在源碼里面迷失。

方法論之關注調用棧

自己 Debug 的過程就是不斷的打斷點的過程。

我再說一次:自己 Debug 的過程就是不斷的打斷點的過程。

打斷點大家都會打,斷點打在哪些地方,這個玩意就很講究了。

在我們的這個 Demo 下,第一個斷點的位置非常好判斷,就打在事務方法的入口處:

圖片

一般來說,大家調試業(yè)務代碼的時候,都是順著斷點往下調試。但是當你去閱讀框架代碼的時候,你得往回看。

什么是“往回看”呢?

當你的程序在斷點處停下的時候,你會發(fā)現(xiàn) IDEA 里面有這樣的一個部分:

圖片

這個調用棧是你在調試的過程中,一個非常非常非常重要的部分。

它表示的是以當前斷點位置為終點的程序調用鏈路。

為了讓你徹底的明白這句話,我給你看一張圖:

圖片

我在 test6 方法中打上斷點,調用棧里面就是以 test6 方法為終點到 main 方法為起點的程序調用鏈接。

當你去點擊這個調用棧的時候,你會發(fā)現(xiàn)程序也會跟著動:

圖片

“跟著動”的這個動作,你可以理解為你站著斷點處“往回看”的過程。

當你理解了調用棧是干啥的了之后,我們再具體看看在當前的 Demo 下,這個調用棧里面都有寫啥:

圖片

標號為 ① 的地方,是 TestController 方法,也就是程序的入口。

標號為 ② 的地方,從包名稱可以看出是 String AOP 相關的方法。

標號為 ③ 的地方,就可以看到是事務相關的邏輯了。

標號為 ④ 的地方,是當前斷點處。

好,到這里,我想讓你簡單的回顧一下你來調試代碼的目的是什么?

是不是想要知道 Spring 的 @Transactional 注解對于異常是否應該回滾的判斷邏輯具體是怎么樣的。

那么,我們是不是應該主要把關注的重點放在標號為 ③ 的地方?

也就是對應到這一行:

圖片

這個地方我一定要特別的強調一下:要保持目標清晰,很多人在源碼里面迷失的原因就是不知不覺間被源碼牽著走遠了。

比如,有人看到標號為 ② 的部分,也就是 AOP 的部分,一想著這玩意我眼熟啊,書上寫過 Spring 的事務是基于 AOP 實現(xiàn)的,我去看看這部分代碼吧。

當你走到 AOP 里面去的時候,路就開始有點走偏了。你明白我意思吧?

即使在這個過程中,你翻閱了這部分的源碼,確實了解到了更多的關于 AOP 和事務之間的關系,但是這個部分并不解決你“關于回滾的判斷”這個問題。

然而更多更真實的情況可能是這樣的,當你點到 AOP 這部分的時候,你一看這個類名稱是 CglibAopProxy:

圖片

你一細嗦,Cglib 你也熟悉啊,它和 JDK 動態(tài)代理是一對好兄弟,都是老八股了。

然后你可能又會點擊到 AopProxy 這個接口,找到 JdkDynamicAopProxy:

圖片

接著你恍然大悟:哦,我在什么都沒有配置的情況下,當前版本的 SpringBoot 默認使用的是 Cglib 作為動態(tài)代理的實現(xiàn)啊。

誒,我怎么記得我背的八股文默認是使用 JDK 呢?

網(wǎng)上查一下,查一下。

哦,原來是這么一回事兒?。?/p>

  • SpringBoot 1.x,默認使用的是 JDK 動態(tài)代理。
  • SpringBoot 2.x 開始,為了解決使用 JDK 動態(tài)代理可能導致的類型轉化異常而默認使用 CGLIB。
  • 在 SpringBoot 2.x 中,如果需要默認使用 JDK 動態(tài)代理可以通過配置項spring.aop.proxy-target-class=false來進行修改,proxyTargetClass配置已無效。

剛剛提到了一個 spring.aop.proxy-target-class 配置,這是個啥,咋配置???

查一下,查一下...

喂,醒一醒啊,朋友,走遠了啊。還記得你調試源碼的目的嗎?

如果你對于 AOP 這個部分感興趣,可以先進行簡單的記錄,但是不要去深入的追蹤。

不要覺得自己只是隨便看看,不要緊。反正正是因為這些“隨便看看”導致你在源碼里面忙了半天感覺這波學到了,但是停下來一想:我 TM 剛剛看了些啥來著?我的問題怎么還沒解決?

我為什么要把這部分非常詳盡,甚至于接近啰嗦的寫一遍,就是因為這個就是初看源碼的朋友最容易犯的錯誤。

特別強調一下:抓住主要矛盾,解決主要問題。

好,回到我們通過調用棧找到的這個和事務相關的方法中:

org.springframework.transaction.interceptor.TransactionInterceptor#invoke

圖片

這個方法,就是我們要打第二個斷點,或者說這才是真正的第一個斷點的地方。

然后,重啟項目,重新發(fā)起請求,從這個地方就可以進行正向的調試,也就是從框架代碼一步步的往業(yè)務代碼執(zhí)行。

比如這個方法接著往下 Debug,就來到了這個地方:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

圖片

找到了這個地方,你就算是無限的接近于問題的真相了。

這個部分我肯定會講的,但是在這里先按下不表,畢竟這并不是本文最重要的東西。

本文最重要的是,我再次重申一遍:我試圖想要教會你一種閱讀源碼的方式,讓你找到一個好的切入點,或者說突破口。

由于這個案例比較簡單,所以很容易找到真正的第一個利于調試的斷點。

如果遇到一些復雜的場景、響應式的編程、異步的調用等等,可能會循環(huán)往復的執(zhí)行上面的動作。

分析調用棧,打斷點,重啟。

再分析調用棧,再打斷點,再重啟。

方法論之死盯日志

其實我發(fā)現(xiàn)很少有人會去注意框架打印的日志,就像是很少有人會去仔細閱讀源碼上的 Javadoc 一樣。

但是其實通過觀察日志輸出,也是一個很好的尋找閱讀源碼突破口的方式。

我們要做的,就是保證 Demo 盡量的單純,不要有太多的和本次排查無關的代碼和依賴引入。

然后把日志級別修改為 debug:

logging.level.root=debug

接著,就是發(fā)起一次調用,然后耐著性子去看日志。

還是我們的這個 Demo,發(fā)起一次調用之后,控制臺輸出了很多的日志,我給你搞個縮略圖看看:

圖片

我們已知的是這里面大概率是有線索的,有沒有什么方法盡量快的找出來呢?

有,但是通用性不強。所以如果經(jīng)驗不夠豐富的話,那么最好的方法就是一行行的去找。

前面我也說過了:啃源碼的過程,一定是非??菰锏?。

所以你一定會找到這樣的日志輸出:

Acquired Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] to manual commit
...
==> Preparing: insert into student ( name, home ) values ( ?, ? )
HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
==> Parameters: why(String), 草市街199號-insertTestNoRollbackFor(String)
<== Updates: 1
...
Committing JDBC transaction on Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c]
Releasing JDBC Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] after transaction

這幾行日志,不就是正對應著 Spring 事務的開啟和提交嗎?

有了日志,我們完全可以基于日志去找對應的日志輸出的地方,比如我們現(xiàn)在要找這一行日志輸出對應的代碼:

o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] for JDBC transaction

首先,我們可以根據(jù)日志知道對應輸出的類是 DataSourceTransactionManager 這個類。

然后找到這個類,按照關鍵詞搜索:

圖片

不就找到這一行代碼了嗎?

或者我們直接秉承大力出奇跡的真理,來一個暴力的全局搜索,也是能搜到這一行代碼的:

圖片

再或者修改一下日志輸出格式,把行號也搞出來嘛。

當我們把日志格式修改為這樣之后:

logging.pattern.cnotallow=%d{dd-MM-yyyy HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %logger.%M:%L - %msg%n

控制臺的日志就變成了這樣:

org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin:263 - Acquired Connection [HikariProxyConnection@1569067488 wrapping com.mysql.cj.jdbc.ConnectionImpl@19a49539] for JDBC transaction

很直觀的就看出來了,這行日志是 DataSourceTransactionManager 類的 doBegin 方法,在 263 行輸出的。

然后你找過去,發(fā)現(xiàn)沒有任何毛病,這就是案發(fā)現(xiàn)場:

圖片

我前面給你說這么多,就是為了讓你找到這一行日志輸出的地方。

現(xiàn)在,找到了,然后呢?

然后肯定就是在這里打斷點,然后重啟程序,重新發(fā)起調用了啊。

這樣,你又能得到一個調用棧:

圖片

然后,你會從調用棧中看到一個我們熟悉的東西:

圖片

朋友,這不就和前面寫的“方法論之關注調用棧”呼應起來了嗎?

這不就是一套組合拳嗎,不就是沉淀出的可復用的方法論嗎?

黑話,咱們也是可以整兩句的。

方法論之查看被調用的地方

除了前面兩種方法之外,我有時候也會直接看我要閱讀部分的方法,在框架中被哪些地方調用了。

比如在我們的 Demo 中,我們要閱讀的代碼非常的明確,就是 @Transactional 注解。

于是直接看一下這個注解在哪些地方用到了:

圖片

有的時候調用的地方會非常的少,甚至只有一兩處,那么直接在調用的地方打上斷點就對了。

雖然 @Transactional 注解一眼望去也是有很多的調用,但是仔細一看大多是測試類。排除測試類、JavaDoc 里面的備注和自己項目中的使用之后,只剩下很明顯的這三處:

圖片

看起來很接近真相,但是很遺憾,這里只是在項目啟動的時候解析注解而已。和我們要調研的地方,差的還有點遠。

這個時候就需要一點經(jīng)驗了,一看苗頭不對,立馬轉換思路。

什么是苗頭不對呢?

你在這幾個地方大上斷點了,只是在項目啟動的過程中斷點起作用了,發(fā)起調用的時候并沒有在斷點處停下,說明發(fā)起調用的時候并不會觸發(fā)這部分邏輯,苗頭不對。

順著這個思路想,在我的 Demo 中拋出了異常,那么 rollbackFor 和 noRollbackFor 這兩個參數(shù)大概率是會在調用的時候被用到,對吧?

圖片

所以當你去看 rollbackFor 被調用的時候只有我們自己寫的業(yè)務代碼在調用:

圖片

怎么辦呢?

這個時候就要靠一點運氣了。

是的,靠運氣。

你都點到 rollbackFor 這個方法來了,你也看了它被調用的地方,在這個過程中你大概率會瞟到幾眼它對應的 JavaDoc:

org.springframework.transaction.annotation.Transactional#rollbackFor

圖片

然后你會發(fā)現(xiàn)在 JavaDoc 里面提到了 rollbackOn 這個方法:

org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable)

圖片

到這里一看,你發(fā)現(xiàn)這是一個接口,它有好多個實現(xiàn)類:

圖片

怎么辦呢?

早期的時候,由于不知道具體的實現(xiàn)類是哪個,我是在每個實現(xiàn)類的入口處都打上斷點,雖然是笨辦法,但是總是能起作用的。

后來我才發(fā)現(xiàn),原來可以直接在接口上打斷點:

圖片

然后,重啟項目,發(fā)起調用,第一次會停在我們方法的入口:

圖片

F9,跳過當前斷點之后,來到了這個地方:

圖片

這里就是我前面在接口上打的方法斷點,走到了這個實現(xiàn)類中:

org.springframework.transaction.interceptor.DelegatingTransactionAttribute

然后,關鍵的就來了,我們又有一個調用棧了,又從調用棧中看到一個我們熟悉的東西:

圖片

朋友,組合拳這不又打起來了?突破口不就又找到了?

關于“瞟到幾眼對應的 JavaDoc ,然后就可能找到突破口”的這個現(xiàn)象,早期對我來說確實是運氣,但是現(xiàn)在已經(jīng)是一個習慣了。一些知名框架的 JavaDoc 真的寫的很清楚的,里面隱藏了很多關鍵信息,而且是最權威的正確信息,讀官網(wǎng)文檔,比讀技術博客穩(wěn)當?shù)亩唷?/p>

探索答案

前面我介紹的都是找到代碼調試突破口的方法。

現(xiàn)在突破口也有了,接下來應該怎么辦呢?

很簡單,調試,反復的調試。從這個方法開始,一步一步的調試:

org.springframework.transaction.interceptor.TransactionInterceptor#invoke

圖片

如果你真的想要有所收獲的話,這是一個需要你親自去動手的步驟,必須要有逐行閱讀的一個過程,然后才能知道大概的處理流程。

我就不進行詳細解讀了,只是把重點給大家畫一下:

圖片

框起來的部分,就是去執(zhí)行業(yè)務邏輯,然后基于業(yè)務邏輯的處理結果,去走不同的邏輯。

拋異常了,走這個方法:completeTransactionAfterThrowing

正常執(zhí)行完畢了,走這個方法:commitTransactionAfterReturning

所以,我們問題的答案就藏在 completeTransactionAfterThrowing 里面。

繼續(xù)調試,進入這個方法之后,可以看到它拿到了事務和當前異常相關的信息:

圖片

在這個方法里面,大體的邏輯是當標號為 ① 的地方為 true 的時候,就在標號為 ② 的地方回滾事務,否則就在標號為 ③ 的地方提交事務:

圖片

因此,標號為 ① 的部分就很重要了,這里面就藏著我們問題的答案。

另外,在這里多說一句,在我們的案例中,這個方法,也就是當前調試的方法是不會回滾的:

圖片

而這個方法是會回滾的:

圖片

也就是這兩個方法在這個地方會走不同的邏輯,所以你在調試的時候遇到 if-else 就需要注意,去構建不同的案例,以覆蓋盡量多的代碼邏輯。

繼續(xù)往下調試,會進入到標號為 ① 的 rollbackOn 方法里面,來到這個方法:

org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn

圖片

這里,就藏著問題的終極答案,而且這里面的代碼邏輯相對比較的繞。

核心邏輯就是通過循環(huán) rollbackRules,這里面裝的是我們在代碼中配置的回滾規(guī)則,在循環(huán)體中拿 ex,也就是我們程序拋出的異常,去匹配規(guī)則,最后選擇一個 winner:

圖片

如果 winner 為空,則走默認邏輯。如果是 RuntimeException 或者是 Error 的子類,就要進行回滾:

圖片

如果有 winner,判斷 winner 是否是不用回滾的配置,如果是,則取反,返回 false,表示不進行回滾:

圖片

那么問題的冠軍就在于:winner 怎么來的?

答案就藏著這個遞歸調用中:

圖片

一句話描述就是:看當前拋出的異常和配置的規(guī)則中的 rollbackFor 和 noRollbackFor 誰距離更近。這里的距離指的是父類和子類之間的關系。

比如,還是這個案例:

圖片

我們拋出的是 RuntimeException,它距離 noRollbackFor=RuntimeException.class 為 0。RuntimeException 是 Exception 的子類,所以距離 rollbackFor = Exception.class 為 1。

所以,winner 是 noRollbackFor,能明白吧?

然后,我們再看一下這個案例:

圖片

根據(jù)前面的“距離”的分析,NullPointerException 是 RuntimeException 的子類,它們之間的距離是 1。而 NullPointerException 到 Exception 的距離是 2:

圖片

所以,rollbackFor=RuntimeException.class 這個的距離更短,所以 winner 是 rollbackFor。

而把 winner 放到這個判斷中,返回是 true:

return !(winner instanceof NoRollbackRuleAttribute);

所以,這就是它為什么會回滾的原因:

好了,到這里你有可能是暈的,暈就對了,去調試這部分代碼,親自摸一遍,你就搞的明明白白了。

最后,再給“死盯日志”的方法論打個補丁吧。

前面我說了,日志級別調整到 Debug 也需要會有意外發(fā)現(xiàn)?,F(xiàn)在,我要再給你說一句,如果 Debug 沒有查到信息,可以試著調整到 trace:

logging.level.root=trace

比如,當我們調整到 trace 之后,就可以看到“ winner 到底是誰”這樣的信息了:

圖片

當然了,trace 級別下日志更多了。

所以,來,再跟我大聲的讀一遍:

啃源碼的過程,一定是非??菰锏模貏e是啃自己接觸不多的框架源碼的時候,千頭萬緒,也得下手去捋,所以一定要耐得住寂寞才行。

作業(yè)

我前面主要是試圖教你一種閱讀源碼時,尋找突破點的技能。這個突破點,說白了就是第一個有效的斷點到底應該打在哪里。

你用前面我教的方法,也能把 @Cacheable 和 @Async 都玩明白。因為它們的底層邏輯和 @Transactional 是一樣的。

所以,現(xiàn)在布置兩個作業(yè)。

拿著這套組合拳,去上手玩一玩 @Cacheable 和 @Async 吧,沉淀出屬于自己的方法論。

@Cacheable:

圖片

@Async:

圖片

本文轉載自微信公眾號「 why技術」,可以通過以下二維碼關注。轉載本文請聯(lián)系 why技術公眾號。??

??

??



責任編輯:武曉燕 來源: why技術
相關推薦

2023-11-30 08:27:38

泛化調用架構

2019-10-17 19:15:22

jQueryJavaScript前端

2020-11-13 08:14:28

JavaScript

2021-02-19 19:35:53

SVG 形狀元素

2023-06-21 00:10:17

JSONWeb服務器JavaScript

2021-02-17 20:40:22

SVG圖像模式

2021-03-02 18:35:27

SVG開發(fā)空間

2021-03-17 09:59:26

Python函數(shù)調用

2024-04-11 12:57:55

Python函數(shù)

2021-07-10 10:01:37

Python簡單函數(shù)

2021-12-28 09:27:45

Javascript 高階函數(shù)前端

2023-10-08 19:06:41

2021-05-29 10:20:54

GoModules語言

2021-02-24 10:14:04

PythonClassPython基礎

2020-12-01 09:36:35

SVG元素屬性

2020-02-24 21:50:24

瓶頸數(shù)據(jù)庫

2021-09-15 10:00:33

Go語言Modules

2020-12-16 08:07:28

語言基礎反射

2021-03-19 10:01:41

SVG畫多邊形Htm基礎

2019-01-30 13:44:34

JVM內存服務器
點贊
收藏

51CTO技術棧公眾號