異常一個神奇的東西,讓廣大程序員對它人又愛又恨。
愛它,經過它能快速定位錯誤,通過層層磨難能學到不少逼坑大法。
恨他,快下班的時刻,週末的早晨,它踏着七彩雲毫無徵兆的來了。
java
今天,要聊的是它的一項神技 : 輔助源碼分析
。
對的,沒有聽錯,它有此功效,只不過咱們被恨衝昏了頭腦,沒看到它的美。mysql
講以前,先簡要鋪墊下須要用到的相關知識。程序員
瞭解點jvm知識都應該知道每一個線程有本身的JVM Stack,程序運行時,會將方法一個一個壓入棧,即棧幀,執行完再彈出棧。以下圖。不知道也不要緊,如今你也知道了,這是第一點。web
public class Sample {
public static void main(String[] args) {
hello();
}
public static void hello(){
StackTraceElement[] traceElements = Thread.currentThread().getStackTrace();
for(StackTraceElement traceElement : traceElements){
System.err.println(traceElement.getMethodName());
}
}
}
複製代碼
輸出結果以下:面試
getStackTrace
hello
main
複製代碼
能夠看到,通上面圖中的入棧過程是一致的,惟一區別是多了個getStackTrace的方法,由於咱們在hello方法內部調用了。也會入棧。
spring
上面說了,是每一個線程有本身的方法棧,因此若是在一個線程調用了另外一個線程,那麼兩個線程有各自的方法棧。不廢話,上代碼。sql
public class Sample {
public static void main(String[] args) {
hello();
System.err.println("--------------------");
new Thread(){
@Override
public void run() {
hello();
}
}.start();
}
public static void hello(){
StackTraceElement[] traceElements = Thread.currentThread().getStackTrace();
for(StackTraceElement traceElement : traceElements){
System.err.println("Thread:" + Thread.currentThread().getName() + " " + traceElement.getMethodName());
}
}
}
複製代碼
輸出結果以下:apache
Thread:main getStackTrace
Thread:main hello
Thread:main main
--------------------
Thread:Thread-0 getStackTrace
Thread:Thread-0 hello
Thread:Thread-0 run
複製代碼
能夠看到,分別在主線程和新開的線程中調用了hello方法,輸出的調用棧是各自獨立的。瀏覽器
若是程序出現異常,會從出現異常的方法沿着調用棧逐步往回找,直到找到捕獲當前異常類型的代碼塊,而後輸出異常信息。代碼以下。spring-mvc
public class Sample {
public static void main(String[] args) {
hello();
}
public static void hello(){
int[] array = new int[0];
array[1] = 1;
}
}
複製代碼
方法執行後的異常以下
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1
at com.yuboon.fragment.exception.Sample.hello(Sample.java:15)
at com.yuboon.fragment.exception.Sample.main(Sample.java:10)
複製代碼
對比上面第一點的執行結果,是否是有些類似。
好了,基礎知識先鋪墊到這。
基於上面的鋪墊,下來咱們先快速試一把,看看效果。
場景是這樣的,不知到你們是否瞭解springboot啓動時是如何加載嵌入的tomcat的,可能不少人專門看過,但估計這會也忘得差很少了。
下面咱們利用異常來快速找到它的啓動加載邏輯。
what ? 異常在哪呢,我正常啓動也沒異常啊。
是滴,正常啓動是沒有,那我能不能讓它不正常啓動呢?
一個正常的狀況下,異常都是被動出現的,也就是非編碼人員的主觀意願出來的。
如今咱們要主動讓它出來,讓它來告訴咱們一些真相。
怎麼讓springboot啓動加載tomcat時出錯,都在jar包裏,也改不了代碼啊,直接調試源碼?仍是debug。不急。
我來告訴你們一個最簡單的方式,利用端口。也就是將tomcat的啓動端口改爲一個已經被使用的端口,好比說你電腦如今運行着一個mysql服務,那我就讓tomcat監聽3306端口,這樣啓動必定會報端口被佔用異常。
來,咱們試一下。將springboot配置文件中的服務端口改爲3306,啓動。
哇哦,想要的異常出來了,多麼熟悉的畫面。
先大概解釋下這個異常信息,整體包含兩段異常信息。
第一段是springboot啓動時內部的異常棧信息,第二段是Tomcat內部加載的異常棧信息。
二者關係就是,由於Tomcat端口被佔用,拋出了端口被佔用異常,進而致使springboot啓動異常。兩段異常的銜接點就在整個異常信息的第一行和最後一行,即Connector.java:1008
Connector.java:1005
處。
圖中藍色標出的類是咱們程序的運行起點。點進去看實際上就是run方法處出了異常。
@SpringBootApplication
public class FragmentExceptionApplicatioin {
public static void main(String[] args) {
SpringApplication.run(FragmentExceptionApplicatioin.class, args);
}
}
複製代碼
既然是分析springboot是如何加載tomcat的,那麼主要分析第一段就OK了,第二段異常信息暫時就能夠忽略。
下面咱們仔細分析分析。回想前情鋪墊裏 一、3 部分的內容,再加上這個異常堆棧信息,咱們就從這個中找到程序的執行順序,進而分析出核心執行流程。找到源碼內部的執行邏輯。
來一步步看下
通過上面的分析,實際上咱們找到了程序運行的起點,即springboot的run方法。且稱爲起始位置
。
下面要找到終點,就是最上面的那一行,且稱爲終點位置
。
at org.apache.catalina.connector.Connector.startInternal(Connector.java:1008) ~[tomcat-embed-core-9.0.21.jar:9.0.21]
複製代碼
有了起點和終點,咱們知道,兩點之間,線段最短。哦,跑題了。
是有了起點和終點,執行過程不就在中間嗎。
再一點點看,分析類圖能夠看到AbstractApplicationContext和ServletWebServerApplicationContext是父子類,因此將出現AbstractApplicationContext的地方都替換爲爲ServletWebServerApplicationContext,最終結合上面的異常棧,咱們能夠繪製出這麼一張時序圖。
簡單組織語言表述一下主體流程,細節暫不展開描述。
應用啓動的run方法調用了SpringApplication的一系列重載run方法以後
調用了SpringApplication的刷新上下文方法和刷新方法
再調用ServletWebServerApplicationContext的刷新方法
ServletWebServerApplicationContext刷新方法再調用內部的finishRefresh方法
finishRefresh調用內部的startWebServer方法
startWebServer內部調用TomcatWebServer的start方法啓動
複製代碼
友情提醒 分析一個陌生框架的源碼,切勿一頭扎進細節,保你進去出來後一臉懵逼。應該先找到程序的執行主線,而找到主線的方法一個是官方文檔的相關介紹,一個是debug,而最直接有效的莫過於利用異常棧。
你們能夠找一款框架親自試試看。
今後不再怕面試官問我某某框架的執行原理了。
分析源碼時有了這個主線,再去分析裏面的細節就容易得多了。不再怕debug進去後不知調用深淺,迷失在源碼當中。
上面只是小試牛刀,下面再看一個例子,經過異常分析下springmvc的執行過程。
呀,這可怎麼搞,上面造個啓動異常,端口重用還想了半天,這個異常要怎麼造。異常出在哪裏才能看到完整的異常棧呢?
不急,根據上面的兩點之間線段最短原理,那天然是找到程序執行的起始位置
和終點位置
了。
這個場景控制器起點貌似在調用端呀。好比pc端?移動端發了個請求過來,那裏是起點呀,我去那裏搞麼。
要這麼複雜,我也就不寫這篇文章了。
媽媽呀,那怎麼搞,我好像有點懵逼了呢!
先看張草圖
既然分析的是springmvc處理過程,也就是說從瀏覽器到tomcat這段咱們是不用管的,咱們只須要分析服務端線程調用springmvc方法後執行的這一段就能夠了。
爸爸呀,服務端執行這個在tomcat裏面呀,我怎麼找。
上面說了先找到起點和終點,沒說兩個都要找到呀,既然起點在tomcat裏很差找,那終點能找到嗎?
我想一想,終點難道是controller裏的方法嗎?
答對了,請求所抵達的終點就是controller裏面聲明的方法。
好的終點找到了,如何報錯,一時腦殼懵逼,哎,仍是不習慣主動寫個異常,一時不知道代碼怎麼寫。
好吧,那咱們就用兩行代碼來主動造個異常,異常水平的高低不要求,能出錯的異常就是好異常。嗯?好像是個病句,不重要。
@RequestMapping("/hello")
public String hello(String name){
String nullObject = null;
nullObject.toString();
return "hello : " + name;
}
複製代碼
OK,寫完了,執行時第四行必報空指針錯誤,啓動測試一下唄。
噹噹噹當,看看,異常棧又來了,此次看着異常是否親切了些。
來分析一波,上面的草圖中能夠看到,線程中確定會調用springmvc的代碼,tomcat的一些處理咱們能夠忽略,直接從異常棧中找org,springframework包開頭的類信息。能夠看到FrameworkServlet
類是由tomcat進入springmvc框架的第一個類。調用它的是HttpServlet
,再順着網上看,就能夠看到DispatcherServlet
,在未使用springboot以前,咱們使用springmvc框架還須要在web.xml中添加配置
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
複製代碼
經過類關係分析,發現三者是繼承關係,DispatcherServlet
爲最終子類。因此在隨後的異常棧分析中,咱們可使用子類去替換父類。也就是異常棧中出現FrameworkServlet、HttpServlet
都可使用DispatcherServlet
進行替換分析。
如此咱們便找到了起始位置
,那接下來的問題就是順着DispatcherServlet
繼續往下分析。
下來須要肯定真正的終點位置
,上面不是肯定了嗎?
上面所肯定的終止位置並非真正的終點位置
,看下面這段異常
發現是個反射調用的異常,那就能夠知道Controller的方法是經過反射調用的,咱們排除JDK自身存在BUG的這種問題,因此這裏其實也能夠忽略,那麼真正的終點位置就是調用反射代碼執行方法的那一行,在哪呢?在這
至此咱們就能夠鎖定終點位置是InvocableHandlerMethod.doInvoke
。
那麼剩下須要具體分析的過程以下圖,也就是搞清楚這幾個方法間的調用關係,處理邏輯,基本上就搞清楚了springmvc是如何接受處理一個請求的邏輯。
再次分析處理類的類圖圖發現
RequestMappingHandlerAdapter爲AbstractHandlerMethodAdapter的子類。
ServletInvocableHandlerMethod爲InvocableHandlerMethod的子類。
同上面同樣,存在父子關係,用最終子類替換父類進行分析。
因此異常棧中出現AbstractHandlerMethodAdapter的地方均可使用RequestMappingHandlerAdapter進行替換。
異常棧中出現InvocableHandlerMethod的地方均可使用ServletInvocableHandlerMethod進行替換。
結合起來畫個時序圖
這樣看執行過程是不清楚了許多。簡要語言表述此處就免了。
回過頭,在看下起始位置
是個線程,回想前情鋪墊裏的第2點,這就合理的解釋了爲何是線程開頭,由於在tomcat處理請求時,開啓了線程,這個線程它有本身的JVM Stack,而這個請求處理的起點即是該線程的run方法。
具體代碼內部細節根據實際狀況具體分析,須要注意的是子類上的方法有些繼承自父類或直接調用的父類,分析的時候爲告終構清晰咱們將父類所有換成了子類,因此這個在具體分析代碼的時候須要注意,直接看子類可能會找不到一些方法,須要結合父類去看,這裏就不帶你們一行一行去分析了,否則我該寫到天亮去了,此文的關鍵是提供一種思路。
等等,這只是請求接受處處理,數據是如何組裝返回前臺的,響應處理呢? 怎麼沒看到,確實。這個流程裏沒有,那如何能看到請求響應的處理流程能,很簡單,只須要在數據返回時造個異常就好了。怎麼造?本身不妨琢磨琢磨先。
但願經過此文能幫你在源碼分析的道路上走的容易些,也但願你們在看到異常不光有恨意,還帶有一絲絲愛意,那我寫這篇文章的目的就達到了。
再送你們修煉此功法的三點關鍵祕訣
此功法法成功的關鍵是找到正確的異常棧輸出位置,一般狀況下是程序執行邏輯終點的那個方法。
多找幾個框架,多找幾個場景,去適應這種思路,所謂孰能生巧。
注意抽象類和其子類,分析時出現抽象類的地方均可使用子類進行替換
友情提醒 此功法還可用在項目業務場景下,剛接手了新的項目,不知如何下手,找不到執行邏輯?debug半天仍是沒有頭緒,不妨試試此法。
它踩着七彩雲走了,留給咱們無盡的遐想。不行,我得趕忙找個框架試一波。
此文風,第一次嘗試,若是以爲不錯不妨動動手指點個小贊,鼓勵下做者,我會努力多寫幾篇。
若是以爲通常,麼關係,我還有屌絲系列,少女系列,油膩男系列等風格。
此文結束,然而精彩故事未完........