[ Coding七十二絕技 ] 如何利用Java異常快速分析源碼

前言

異常一個神奇的東西,讓廣大程序員對它人又愛又恨。
愛它,經過它能快速定位錯誤,通過層層磨難能學到不少逼坑大法。
恨他,快下班的時刻,週末的早晨,它踏着七彩雲毫無徵兆的來了。
java

3.jpg

今天,要聊的是它的一項神技 :  輔助源碼分析
對的,沒有聽錯,它有此功效,只不過咱們被恨衝昏了頭腦,沒看到它的美。mysql

降龍.gif

前情鋪墊

講以前,先簡要鋪墊下須要用到的相關知識。程序員

1

瞭解點jvm知識都應該知道每一個線程有本身的JVM Stack,程序運行時,會將方法一個一個壓入棧,即棧幀,執行完再彈出棧。以下圖。不知道也不要緊,如今你也知道了,這是第一點。web

未命名文件.png

Java中獲取線程的方法調用棧,可經過以下方式

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

2

上面說了,是每一個線程有本身的方法棧,因此若是在一個線程調用了另外一個線程,那麼兩個線程有各自的方法棧。不廢話,上代碼。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方法,輸出的調用棧是各自獨立的。瀏覽器

3

若是程序出現異常,會從出現異常的方法沿着調用棧逐步往回找,直到找到捕獲當前異常類型的代碼塊,而後輸出異常信息。代碼以下。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 ? 異常在哪呢,我正常啓動也沒異常啊。

是滴,正常啓動是沒有,那我能不能讓它不正常啓動呢?

一個正常的狀況下,異常都是被動出現的,也就是非編碼人員的主觀意願出來的。

如今咱們要主動讓它出來,讓它來告訴咱們一些真相。

真相只有一個.gif


怎麼讓springboot啓動加載tomcat時出錯,都在jar包裏,也改不了代碼啊,直接調試源碼?仍是debug。不急。

我來告訴你們一個最簡單的方式,利用端口。也就是將tomcat的啓動端口改爲一個已經被使用的端口,好比說你電腦如今運行着一個mysql服務,那我就讓tomcat監聽3306端口,這樣啓動必定會報端口被佔用異常。

來,咱們試一下。將springboot配置文件中的服務端口改爲3306,啓動。

image.png

哇哦,想要的異常出來了,多麼熟悉的畫面。

image.png

先大概解釋下這個異常信息,整體包含兩段異常信息。

第一段是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,最終結合上面的異常棧,咱們能夠繪製出這麼一張時序圖。

tomcat.png


能夠清楚的看到啓動時加載的過程。如何?清不清楚。

簡單組織語言表述一下主體流程,細節暫不展開描述。

應用啓動的run方法調用了SpringApplication的一系列重載run方法以後
調用了SpringApplication的刷新上下文方法和刷新方法
再調用ServletWebServerApplicationContext的刷新方法
ServletWebServerApplicationContext刷新方法再調用內部的finishRefresh方法
finishRefresh調用內部的startWebServer方法
startWebServer內部調用TomcatWebServer的start方法啓動
複製代碼

友情提醒 分析一個陌生框架的源碼,切勿一頭扎進細節,保你進去出來後一臉懵逼。應該先找到程序的執行主線,而找到主線的方法一個是官方文檔的相關介紹,一個是debug,而最直接有效的莫過於利用異常棧。

你們能夠找一款框架親自試試看。
今後不再怕面試官問我某某框架的執行原理了。

分析源碼時有了這個主線,再去分析裏面的細節就容易得多了。不再怕debug進去後不知調用深淺,迷失在源碼當中

功法進階

上面只是小試牛刀,下面再看一個例子,經過異常分析下springmvc的執行過程。

呀,這可怎麼搞,上面造個啓動異常,端口重用還想了半天,這個異常要怎麼造。異常出在哪裏才能看到完整的異常棧呢?

不急,根據上面的兩點之間線段最短原理,那天然是找到程序執行的起始位置終點位置了。

這個場景控制器起點貌似在調用端呀。好比pc端?移動端發了個請求過來,那裏是起點呀,我去那裏搞麼。

要這麼複雜,我也就不寫這篇文章了。

媽媽呀,那怎麼搞,我好像有點懵逼了呢!

e18d20c94006dfe0-9eef65073f0f6be0-335c5fd1b4bae44534eef19e66fb248b.jpg

先看張草圖

web請求草圖.png

不論是nio bio 又或是aio,服務端最終執行請求,必然會分配一個線程去作。

既然分析的是springmvc處理過程,也就是說從瀏覽器到tomcat這段咱們是不用管的,咱們只須要分析服務端線程調用springmvc方法後執行的這一段就能夠了。

爸爸呀,服務端執行這個在tomcat裏面呀,我怎麼找。

爸爸去哪了.gif

確實這麼找,很差找。

上面說了先找到起點和終點,沒說兩個都要找到呀,既然起點在tomcat裏很差找,那終點能找到嗎?

我想一想,終點難道是controller裏的方法嗎?

答對了,請求所抵達的終點就是controller裏面聲明的方法。

好的終點找到了,如何報錯,一時腦殼懵逼,哎,仍是不習慣主動寫個異常,一時不知道代碼怎麼寫。

好吧,那咱們就用兩行代碼來主動造個異常,異常水平的高低不要求,能出錯的異常就是好異常。嗯?好像是個病句,不重要。

@RequestMapping("/hello")
public String hello(String name){
		String nullObject = null;
		nullObject.toString();
		return "hello : " + name;
}
複製代碼

OK,寫完了,執行時第四行必報空指針錯誤,啓動測試一下唄。

噹噹噹當,看看,異常棧又來了,此次看着異常是否親切了些。

image.png

來分析一波,上面的草圖中能夠看到,線程中確定會調用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進行替換分析。

image.png

如此咱們便找到了起始位置,那接下來的問題就是順着DispatcherServlet繼續往下分析。
下來須要肯定真正的終點位置,上面不是肯定了嗎?
上面所肯定的終止位置並非真正的終點位置,看下面這段異常

image.png

發現是個反射調用的異常,那就能夠知道Controller的方法是經過反射調用的,咱們排除JDK自身存在BUG的這種問題,因此這裏其實也能夠忽略,那麼真正的終點位置就是調用反射代碼執行方法的那一行,在哪呢?在這

image.png

至此咱們就能夠鎖定終點位置是InvocableHandlerMethod.doInvoke

那麼剩下須要具體分析的過程以下圖,也就是搞清楚這幾個方法間的調用關係,處理邏輯,基本上就搞清楚了springmvc是如何接受處理一個請求的邏輯。

image.png

再次分析處理類的類圖圖發現
RequestMappingHandlerAdapter爲AbstractHandlerMethodAdapter的子類。
ServletInvocableHandlerMethod爲InvocableHandlerMethod的子類。
同上面同樣,存在父子關係,用最終子類替換父類進行分析。
因此異常棧中出現AbstractHandlerMethodAdapter的地方均可使用RequestMappingHandlerAdapter進行替換。
異常棧中出現InvocableHandlerMethod的地方均可使用ServletInvocableHandlerMethod進行替換。

結合起來畫個時序圖

springmvc.png

這樣看執行過程是不清楚了許多。簡要語言表述此處就免了。

回過頭,在看下起始位置

image.png

是個線程,回想前情鋪墊裏的第2點,這就合理的解釋了爲何是線程開頭,由於在tomcat處理請求時,開啓了線程,這個線程它有本身的JVM Stack,而這個請求處理的起點即是該線程的run方法。

具體代碼內部細節根據實際狀況具體分析,須要注意的是子類上的方法有些繼承自父類或直接調用的父類,分析的時候爲告終構清晰咱們將父類所有換成了子類,因此這個在具體分析代碼的時候須要注意直接看子類可能會找不到一些方法,須要結合父類去看,這裏就不帶你們一行一行去分析了,否則我該寫到天亮去了,此文的關鍵是提供一種思路。

等等,這只是請求接受處處理,數據是如何組裝返回前臺的,響應處理呢? 怎麼沒看到,確實。這個流程裏沒有,那如何能看到請求響應的處理流程能,很簡單,只須要在數據返回時造個異常就好了。怎麼造?本身不妨琢磨琢磨先。

收工

但願經過此文能幫你在源碼分析的道路上走的容易些,也但願你們在看到異常不光有恨意,還帶有一絲絲愛意,那我寫這篇文章的目的就達到了。

再送你們修煉此功法的三點關鍵祕訣

1

此功法法成功的關鍵是找到正確的異常棧輸出位置,一般狀況下是程序執行邏輯終點的那個方法。

2

多找幾個框架,多找幾個場景,去適應這種思路,所謂孰能生巧。

3

注意抽象類和其子類,分析時出現抽象類的地方均可使用子類進行替換

友情提醒 此功法還可用在項目業務場景下,剛接手了新的項目,不知如何下手,找不到執行邏輯?debug半天仍是沒有頭緒,不妨試試此法。

它踩着七彩雲走了,留給咱們無盡的遐想。不行,我得趕忙找個框架試一波。

此文風,第一次嘗試,若是以爲不錯不妨動動手指點個小贊,鼓勵下做者,我會努力多寫幾篇。

若是以爲通常,麼關係,我還有屌絲系列,少女系列,油膩男系列等風格。

此文結束,然而精彩故事未完........

相關文章
相關標籤/搜索