[ 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半天仍是没有头绪,不妨试试此法。

它踩着七彩云走了,留给咱们无尽的遐想。不行,我得赶忙找个框架试一波。

此文风,第一次尝试,若是以为不错不妨动动手指点个小赞,鼓励下做者,我会努力多写几篇。

若是以为通常,么关系,我还有屌丝系列,少女系列,油腻男系列等风格。

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

相关文章
相关标签/搜索