[ Coding七十二绝技 ] 如何利用Java异常快速分析源码
前言
异常一个神奇的东西,让广大程序员对它人又爱又恨。
爱它,通过它能快速定位错误,经过层层磨难能学到很多逼坑大法。
恨他,快下班的时刻,周末的早晨,它踏着七彩云毫无征兆的来了。
今天,要聊的是它的一项神技 : 辅助源码分析
。
对的,没有听错,它有此功效,只不过我们被恨冲昏了头脑,没看到它的美。
前情铺垫
讲之前,先简要铺垫下需要用到的相关知识。
1
了解点jvm知识都应该知道每个线程有自己的JVM Stack,程序运行时,会将方法一个一个压入栈,即栈帧,执行完再弹出栈。如下图。不知道也没关系,现在你也知道了,这是第一点。
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方法内部调用了。也会入栈。
2
上面说了,是每个线程有自己的方法栈,所以如果在一个线程调用了另一个线程,那么两个线程有各自的方法栈。不废话,上代码。
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());
}
}
}
输出结果如下:
Thread:main getStackTrace
Thread:main hello
Thread:main main
--------------------
Thread:Thread-0 getStackTrace
Thread:Thread-0 hello
Thread:Thread-0 run
可以看到,分别在主线程和新开的线程中调用了hello方法,输出的调用栈是各自独立的。
3
如果程序出现异常,会从出现异常的方法沿着调用栈逐步往回找,直到找到捕获当前异常类型的代码块,然后输出异常信息。代码如下。
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了,第二段异常信息暂时就可以忽略。
下面我们仔细分析分析。回想前情铺垫里 [ 1 ][ 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端?移动端发了个请求过来,那里是起点呀,我去那里搞么。
要这么复杂,我也就不写这篇文章了。
妈妈呀,那怎么搞,我好像有点懵逼了呢!
先看张草图
不管是nio bio 又或是aio,服务端最终执行请求,必然会分配一个线程去做。
既然分析的是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进行替换。
结合起来画个时序图bstractHandlerMethodAdapt
这样看执行过程是不清楚了许多。简要语言表述此处就免了。
回过头,在看下起始位置
是个线程,回想前情铺垫里的第 [ 2 ] 点,这就合理的解释了为什么是线程开头,因为在tomcat处理请求时,开启了线程,这个线程它有自己的JVM Stack,而这个请求处理的起点便是该线程的run方法。
具体代码内部细节根据实际情况具体分析,需要注意的是子类上的方法有些继承自父类或直接调用的父类,分析的时候为了结构清晰我们将父类全部换成了子类,所以这个在具体分析代码的时候需要注意,直接看子类可能会找不到一些方法,需要结合父类去看,这里就不带大家一行一行去分析了,不然我该写到天亮去了,此文的关键是提供一种思路。
等等,这只是请求接受到处理,数据是如何组装返回前台的,响应处理呢? 怎么没看到,确实。这个流程里没有,那如何能看到请求响应的处理流程能,很简单,只需要在数据返回时造个异常就行了。怎么造?自己不妨琢磨琢磨先。
收工
希望通过此文能帮你在源码分析的道路上走的容易些,也希望大家在看到异常不光有恨意,还带有一丝丝爱意,那我写这篇文章的目的就达到了。
再送大家修炼此功法的三点关键秘诀
1
此功法法成功的关键是找到正确的异常栈输出位置,通常情况下是程序执行逻辑终点的那个方法。
2
多找几个框架,多找几个场景,去适应这种思路,所谓孰能生巧。
3
注意抽象类和其子类,分析时出现抽象类的地方都可使用子类进行替换
友情提醒
此功法还可用在项目业务场景下,刚接手了新的项目,不知如何下手,找不到执行逻辑?debug半天还是没有头绪,不妨试试此法。
它踩着七彩云走了,留给我们无尽的遐想。不行,我得赶紧找个框架试一波。
此文风,第一次尝试,如果觉得不错不妨动动手指点个小赞,鼓励下作者,我会努力多写几篇。
如果觉得一般,么关系,我还有屌丝系列,少女系列,油腻男系列等风格。
此文结束,然而精彩故事未完……..
[ Coding七十二绝技 ] 如何利用Java异常快速分析源码的更多相关文章
- (二)一起学 Java Collections Framework 源码之 AbstractCollection
. . . . . 目录 (一)一起学 Java Collections Framework 源码之 概述(未完成) (二)一起学 Java Collections Framework 源码之 Abs ...
- GridView七十二绝技-大全(收藏版)(转至别人博客)
快速预览:GridView无代码分页排序GridView选中,编辑,取消,删除GridView正反双向排序GridView和下拉菜单DropDownList结合GridView和CheckBox结合鼠 ...
- Android 音视频深入 十二 FFmpeg视频替换声音(附源码下载)
项目地址,求starhttps://github.com/979451341/AudioVideoStudyCodeTwo/tree/master/FFmpeg%E7%BB%99%E8%A7%86%E ...
- “全栈2019”Java第七十二章:静态内部类访问外部类成员
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- “全栈2019”Java异常第十二章:catch与异常匹配
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...
- TNFE-Weekly[第七十二周已更新]
前端行业发展飞速,新技术如雨后春笋般快速出现,尤其是各种小程序陆续推出,相关的信息.文章也铺天盖地的遍布在各处,我们有时候会困惑,不知道哪些信息对于自己是有价值的,那么TNFE-腾讯新闻前端团队启动了 ...
- Java集合框架源码(二)——hashSet
注:本人的源码基于JDK1.8.0,JDK的版本可以在命令行模式下通过java -version命令查看. 在前面的博文(Java集合框架源码(一)——hashMap)中我们详细讲了HashMap的原 ...
- Java并发包源码学习系列:阻塞队列BlockingQueue及实现原理分析
目录 本篇要点 什么是阻塞队列 阻塞队列提供的方法 阻塞队列的七种实现 TransferQueue和BlockingQueue的区别 1.ArrayBlockingQueue 2.LinkedBloc ...
- Java文件操作源码大全
Java文件操作源码大全 1.创建文件夹 52.创建文件 53.删除文件 54.删除文件夹 65.删除一个文件下夹所有的文件夹 76.清空文件夹 87.读取文件 88.写入文件 99.写入随机文件 9 ...
随机推荐
- nyoj 4 ASCII码排序
ASCII码排序 时间限制:3000 ms | 内存限制:65535 KB | 难度:2 描述 输入三个字符(可以重复)后,按各字符的ASCII码从小到大的顺序输出这三个字符. 输入 第一 ...
- poj 3281 Dining (Dinic)
Dining Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 22572 Accepted: 10015 Descript ...
- 【集合系列】- 深入浅出的分析TreeMap
一.摘要 在集合系列的第一章,咱们了解到,Map的实现类有HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.Pro ...
- python:调用bash
利用os模块 python调用Shell脚本,有三种方法: os.system(cmd)返回值是脚本的退出状态码 os.popen(cmd)返回值是脚本执行过程中的输出内容 commands.gets ...
- SpringBoot让你的Bean动起来(自定义参数解析HandlerMethodArgumentResolver)
SpringBoot让你的Bean动起来(自定义参数解析HandlerMethodArgumentResolver) 简介 我们 Controller 用到的一些 Bean 需要通过一定的方式去获取的 ...
- usaco training <1.2 Your Ride Is Here>
题面 Your Ride Is Here It is a well-known fact that behind every good comet is a UFO. These UFOs often ...
- WebSocket网络通信协议
WebSocket 协议在2008年诞生,2011年成为国际标准.所有浏览器都已经支持了. HTTP 协议有一个缺陷:通信只能由客户端发起.这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端 ...
- react antd Table动态合并单元格
示例数据 原始数组 const data = [ { key: '0', name: 'John Brown', age:22, address: 'New York No. 1 Lake Park' ...
- (六)OpenStack---M版---双节点搭建---Neutron安装和配置
↓↓↓↓↓↓↓↓视频已上线B站↓↓↓↓↓↓↓↓ >>>>>>传送门 1.创建网络服务数据库 2.获得 admin 凭证来获取只有管理员能执行的命令的访问权限 3.创 ...
- 浅谈集群版Redis和Gossip协议
昨天的文章写了关于分布式系统中一致性哈希算法的问题,文末提了一下Redis-Cluster对于一致性哈希算法的实现方案,今天来看一下Redis-Cluster和其中的重要概念Gossip协议. 1.R ...