给出以下异常信息:java
java.lang.RuntimeException: level 2 exception
at com.msh.demo.exceptionStack.Test.fun2(Test.java:17)
at com.msh.demo.exceptionStack.Test.main(Test.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.io.IOException: level 1 exception
at com.msh.demo.exceptionStack.Test.fun1(Test.java:10)
at com.msh.demo.exceptionStack.Test.fun2(Test.java:15)
... 6 more复制代码
学这么多年Java,你真的会阅读Java的异常信息吗?你能说清楚异常抛出过程当中的事件顺序吗?git
上述异常信息在由一个demo产生:程序员
package com.msh.demo.exceptionStack;
import java.io.IOException;
/** * Created by monkeysayhi on 2017/10/1. */
public class Test {
private void fun1() throws IOException {
throw new IOException("level 1 exception");
}
private void fun2() {
try {
fun1();
} catch (IOException e) {
throw new RuntimeException("level 2 exception", e);
}
}
public static void main(String[] args) {
try {
new Test().fun2();
} catch (Exception e) {
e.printStackTrace();
}
}
}复制代码
此次我复制了完整的文件内容,使文章中的代码行号和实际行号一一对应。github
根据上述异常信息,异常抛出过程当中的事件顺序是:bash
那么,如何阅读异常信息呢?有几点你须要认识清楚:服务器
i+1
个异常是第i
个异常被抛出的缘由cause
,以“Caused by”开头。如今,回过头再去阅读示例的异常信息,是否是至关简单?app
为了帮助理解,我尽量通俗易懂的描述了异常信息的结构和组成元素,可能会引入一些纰漏。阅读异常信息是Java程序猿的基本技能,但愿你能内化它,忘掉这些冗长的描述。框架
若是还不理解,建议你亲自追踪一次异常的建立和打印过程,使用示例代码便可,它很简单但足够。难点在于异常是JVM提供的机制,你须要了解JVM的实现;且底层调用了不少native方法,而追踪native代码没有那么方便。ide
示例的异常信息中,异常名、细节信息、路径三个元素都有,可是,因为JVM的优化,细节信息和路径可能会被省略。测试
这常常发生于服务器应用的日志中,因为相同异常已被打印屡次,若是继续打印相同异常,JVM会省略掉细节信息和路径队列,向前翻阅便可找到完整的异常信息。
猴哥以前使用Yarn的Timeline Server时遇到过该问题。你能体会那种感受吗?卧槽,为何只有异常名没有异常栈?没有异常栈怎么老子怎么知道哪里抛出的异常?线上服务老子又不能停,全靠日志了啊喂!
网上有很多相同的case,好比NullPointerException丢失异常堆栈信息,读者能够参照这个连接实验一下。
为了恰当的表达一个异常,咱们有时候须要自定义异常,并添加一些成员变量,打印异常栈时,自动补充打印必要的信息。
追踪打印异常栈的代码:
...
public void printStackTrace() {
printStackTrace(System.err);
}
...
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}
...
private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set<Throwable> dejaVu =
Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
dejaVu.add(this);
synchronized (s.lock()) {
// Print our stack trace
s.println(this);
StackTraceElement[] trace = getOurStackTrace();
for (StackTraceElement traceElement : trace)
s.println("\tat " + traceElement);
// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}
...复制代码
暂不关心同步问题,可知,打印异常名和细节信息的代码为:
s.println(this);复制代码
JVM在运行期经过动态绑定实现this引用上的多态调用。继续追踪的话,最终会调用this实例的toString()方法。全部异常的最低公共祖先类是Throwable类,它提供了默认的toString()实现,大部分常见的异常类都没有覆写这个实现,咱们自定义的异常也能够直接继承这个实现:
...
public String toString() {
String s = getClass().getName();
String message = getLocalizedMessage();
return (message != null) ? (s + ": " + message) : s;
}
...
public String getLocalizedMessage() {
return getMessage();
}
...
public String getMessage() {
return detailMessage;
}
...复制代码
显然,默认实现的打印格式就是示例的异常信息格式:异常名(全限定名)+细节信息。detailMessage由用户建立异常时设置,所以,若是有自定义的成员变量,咱们一般在toString()方法中插入这个变量。参考com.sun.javaws.exceptions
包中的BadFieldException
,看看它如何插入自定义的成员变量field和value:
public String toString() {
return this.getValue().equals("https")?"BadFieldException[ " + this.getRealMessage() + "]":"BadFieldException[ " + this.getField() + "," + this.getValue() + "]";
}复制代码
严格的说,
BadFieldException
的toString中并无直接插入field成员变量。不过这不影响咱们理解,感兴趣的读者可自行翻阅源码。
根据异常信息debug是程序员的基本技能,这里围绕异常信息的阅读和打印过程做了初步探索,后续还会整理一下经常使用的异常类,结合程序猿应该记住的几条基本规则,更好的理解如何用异常帮助咱们写出clean code。
Java至关完备的异常处理机制是一把双刃剑,用好它能加强代码的可读性和鲁棒性,用很差则会让代码变的更加不可控。例如,在空指针上调用成员方法,运行期会抛出异常,这是很天然的——可是,是不可控的等待它在某个时刻某个位置抛出异常(实际上仍是“肯定”的,但对于debug来讲是“不肯定”的),仍是可控的在进入方法伊始就检查并主动抛出异常呢?进一步的,哪些异常应该被即刻处理,哪些应该继续抛到外层呢?抛往外层时,什么时候须要封装异常呢?看看String#toLowerCase(),看看ProcessBuilder#start(),体会一下。
本文连接:你真的会阅读Java的异常信息吗?
做者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,可是必须保留本文的署名及连接。