目录
异常的生命周期
异常分类
Throwable的成员变量
detailMessage
stackTrace
suppressedExceptions
cause
异常打印
序列化/反序列化
应用
使用try-with-resource自动关闭资源
在一个异常中保留另一个异常
发生异常进行重试
使用Throwable捕获异常
Throwable.java抽象了所有的异常,从异常的生命周期来看,可以分成:
(1)根据类的体系
(2)根据是否在编译期间检查
private String detailMessage;
这个字段用于存储异常的简要说明,可以通过e.getMessage()获得。
private StackTraceElement[] stackTrace = UNASSIGNED_STACK;
这个字段用于存储异常栈。代码的行号会在源码编译时一起编译到字节码中。在运行发生异常时,方法调用栈会写到stackTrace字段中。栈的顶端是异常发生的位置,即throw的位置;栈的底端是线程开始的位置。我们可以通过e.printStackTrace、e.getStackTrace等方法查看异常栈,来分析程序出错的位置在源码中的位置。
stackTrace的初始值是一个常量UNASSIGNED_STACK:
private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
StackTraceElement对象一般是由JVM生成的。他包含4个成员变量:类的全限定名、方法名、类所在的源码中的文件名、行号。
如果希望禁用stackTrace,我们可以在构造方法中设置writableStackTrace=false。
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace)
这是一个protected方法,所以我们自定义的异常类可以去使用这个方法。在某些内存很小的机器上可以节约内存。此时,stackTrace字段被设置为null,并且不可再写入和读取。
在代码设计中,会使用一些特殊量(sentinel)来表示特定的状态。比如上面说到的,stackTrace为UNASSIGNED_STACK代表初始值,为null代表不可访问。Throwable的另外几个成员变量,也采用了这种设计思路。
private List suppressedExceptions = SUPPRESSED_SENTINEL;
suppressedExceptions用来存储其他的不是强关联的异常,防止异常的丢失。比如:
try{
// open an IO, read and write
...
}catch(IOException e){
// handle exception
...
}finally{
// close IO
...
}
如果try执行时发生了异常,代码会跳转到catch块中,并在执行完catch代码后执行finally块。如果finally中执行关闭资源等操作时发生了异常,那么finally中的这个异常会把try中的异常覆盖掉,这样我们就会丢失原来的异常信息。
针对这个问题,我们可以把finally中出现的异常,使用addSuppressed方法添加到e的suppressedExceptions字段中。
private static final List SUPPRESSED_SENTINEL =
Collections.unmodifiableList(new ArrayList(0));
suppressedExceptions的初值是一个长度为0的不可变的List。
当suppressedExceptions=null时,代表不可访问。
private Throwable cause = this;
· cause用来表示产生当前异常的根本原因。可以使用构造方法,或者initCause方法进行设置。
在下面的代码中,如果lowLevel方法抛出了异常,那么catch中重新抛出的异常会将其覆盖,这样我们会丢失最原始的异常信息。
try{
lowLevel();
}catch(Exception e){
throw new HighLevelException("system error");
}
此时我们可以将原始的异常写到HighLevelException的cause字段中。这样原始的异常就不会丢失。
try{
lowLevel();
}catch(Exception e){
throw new HighLevelException("system error", e);
}
这种方法的另一个好处是,可以进行异常的转换。如果一个类实现一个接口的时候,接口没有声明某个受检查的异常,而实现却抛出了这个异常,这是不允许的。此时就可以像上面的代码一样,将受检查的异常转换成不受检查的异常,来符合接口的声明。此外,用一个异常去封装另一个异常,可以使调用方只关注高层的异常,而不必关注底层的实现细节。
cause的初始值是this。
printStackTrace()方法会把Throwable对象和他的异常栈打印到标准错误流中。
也可以使用printStackTrace(PrintStream s)或者printStackTrace(PrintWriter s)打印到指定的输出流中。这两个方法都调用了printStackTrace(PrintStreamOrWriter s)方法,接下来我们详细解读这个方法。
private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set dejaVu =
Collections.newSetFromMap(new IdentityHashMap());
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);
}
}
形式参数的PrintStreamOrWriter是Throwable的内部类。WrappedPrintStream和WrappedPrintedWriter也是Throwable的内部类,实现了这个PrintStreamOrWriter接口,并分别包装了PrintStream和PrintWriter对象。这样设计可以简化代码。
Set dejaVu =
Collections.newSetFromMap(new IdentityHashMap());
在java中,Set是利用Map来实现的,比如HashSet是利用HashMap实现的,TreeSet是利用NavigableHashMap实现的。因此,Set的重复性、有序性、是否支持并发等规则,都和对应的Map相同。
由于IdentityHashMap没有现成的Set和它对应,因此可以使用Collections.newSetFromMap方法来创建一个Set的数据结构,这个Set将根据对象指针是否相同来判断重复性,而不是我们常见的equals和hashCode方法。
在这里,dejaVu用于在递归中判断是否调用了自身,从而防止循环递归。
在打印时,将使用监视器锁锁住打印流对象。
接下来,打印对象的toString方法。
然后打印异常栈。
然后递归打印suppressedExpressions。
然后递归打印cause。
打印结束后,释放锁。
下面是一段代码demo:
package exception;
public class CauseAndSuppressDemo {
public static void main(String[] args) {
RuntimeException e1 = null;
try{
doSomething();
}catch (Exception e){
e1 = new RuntimeException("exception occurs", e);
}finally {
try{
close();
}catch (Exception e2){
if(e1 != null){
e1.addSuppressed(e2);
e1.printStackTrace();
}else {
e2.printStackTrace();
}
}
}
}
private static void doSomething() {
System.out.println(1/0);
}
private static void close() {
System.out.println(Integer.valueOf(null));
}
}
运行上面的代码,可以看到打印的内容:
java.lang.RuntimeException: exception occurs
at exception.CauseAndSuppressDemo.main(CauseAndSuppressDemo.java:13)
Suppressed: java.lang.NumberFormatException: null
at java.lang.Integer.parseInt(Integer.java:542)
at java.lang.Integer.valueOf(Integer.java:766)
at exception.CauseAndSuppressDemo.close(CauseAndSuppressDemo.java:33)
at exception.CauseAndSuppressDemo.main(CauseAndSuppressDemo.java:16)
Caused by: java.lang.ArithmeticException: / by zero
at exception.CauseAndSuppressDemo.doSomething(CauseAndSuppressDemo.java:29)
at exception.CauseAndSuppressDemo.main(CauseAndSuppressDemo.java:11)
这儿注意到,suppressedExceptions打印的内容会多一个tab缩进,而cause没有。在分析递归嵌套的异常时,这一点会比较重要。
在printStackTrace(PrintStreamOrWriter s)方法中,调用了getOurStackTrace方法:
private synchronized StackTraceElement[] getOurStackTrace() {
// Initialize stack trace field with information from
// backtrace if this is the first call to this method
if (stackTrace == UNASSIGNED_STACK ||
(stackTrace == null && backtrace != null) /* Out of protocol state */) {
int depth = getStackTraceDepth();
stackTrace = new StackTraceElement[depth];
for (int i=0; i < depth; i++)
stackTrace[i] = getStackTraceElement(i);
} else if (stackTrace == null) {
return UNASSIGNED_STACK;
}
return stackTrace;
}
第一次调用getOurStackTrace方法时,堆栈信息会复制到stackTrace中。后续调用则直接返回stackTrace。
在printStackTrace(PrintStreamOrWriter s)方法中,调用了printEnclosedStackTrace方法:
private void printEnclosedStackTrace(PrintStreamOrWriter s,
StackTraceElement[] enclosingTrace,
String caption,
String prefix,
Set dejaVu) {
assert Thread.holdsLock(s.lock());
if (dejaVu.contains(this)) {
s.println("\t[CIRCULAR REFERENCE:" + this + "]");
} else {
dejaVu.add(this);
// Compute number of frames in common between this and enclosing trace
StackTraceElement[] trace = getOurStackTrace();
int m = trace.length - 1;
int n = enclosingTrace.length - 1;
while (m >= 0 && n >=0 && trace[m].equals(enclosingTrace[n])) {
m--; n--;
}
int framesInCommon = trace.length - 1 - m;
// Print our stack trace
s.println(prefix + caption + this);
for (int i = 0; i <= m; i++)
s.println(prefix + "\tat " + trace[i]);
if (framesInCommon != 0)
s.println(prefix + "\t... " + framesInCommon + " more");
// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION,
prefix +"\t", dejaVu);
// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, prefix, dejaVu);
}
}
这个方法是用来打印suppressedExceptions和cause的。我们看到有一个名字叫enclosingTrace的形式参数,这个是上层的异常对象,比如e1.initCause(e2)或者e1.addSuppressed(d2),则e1就是e2的enclosingTrace。
首先,代码通过判断dejaVu对象跳过了循环调用。
接下来,while语句的这段代码,是为了减少堆栈的重复打印,因为在很多场景中,一个异常的堆栈和它的suppressedExceptions或cause的堆栈有很大一部分是重叠的。以下是demo:
package exception;
public class CauseDemo {
public static class Junk {
public static void main(String args[]) {
a();
}
static void a() {
b();
}
static void b() {
c();
}
static void c() {
try {
d();
} catch (Exception e) {
throw new RuntimeException("ccccc", e);
}
}
static void d() {
e();
}
static void e() {
throw new RuntimeException("eeeee");
}
}
}
运行main方法,打印内容如下:
Exception in thread "main" java.lang.RuntimeException: ccccc
at exception.CauseDemo$Junk.c(CauseDemo.java:25)
at exception.CauseDemo$Junk.b(CauseDemo.java:18)
at exception.CauseDemo$Junk.a(CauseDemo.java:14)
at exception.CauseDemo$Junk.main(CauseDemo.java:10)
Caused by: java.lang.RuntimeException: eeeee
at exception.CauseDemo$Junk.e(CauseDemo.java:34)
at exception.CauseDemo$Junk.d(CauseDemo.java:30)
at exception.CauseDemo$Junk.c(CauseDemo.java:23)
... 3 more
可以看到,cause堆栈最后打印出了 ... 3 more 的字样。这是因为它的异常栈底部的3个元素,和enclosingTrace底部的3个元素是相同的,因此被省略了。
序列化是指Throwable对象写到输出流中,反序列化是指从输入流中构造出Throwable对象。先来看一个demo:
package exception;
import java.io.*;
public class SerdeDemo {
public static void main(String[] args) {
byte[] container = null;
// serialize
try{
doSomething();
}catch (Exception e){
container = writeExceptionToOutputStream(e);
}
// deserialize
Exception e = readExceptionFromInputStream(container);
if(e != null){
e.printStackTrace();
}
}
private static byte[] writeExceptionToOutputStream(Exception e) {
try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)){
oos.writeObject(e);
return baos.toByteArray();
}catch (IOException ie){
throw new RuntimeException(ie);
}
}
private static Exception readExceptionFromInputStream(byte[] container) {
try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(container))){
return (Exception) ois.readObject();
}catch (IOException | ClassNotFoundException ie){
throw new RuntimeException(ie);
}
}
private static void doSomething(){
throw new RuntimeException("aaaaa");
}
}
这段代码把抛出的异常通过ObjectOutputStream序列化到container数组中,然后通过ObjectInputStream从container数组中读取数据,构造出Exception对象。
接下来,分析一下Throwable的writerObject方法。
private synchronized void writeObject(ObjectOutputStream s)
throws IOException {
// Ensure that the stackTrace field is initialized to a
// non-null value, if appropriate. As of JDK 7, a null stack
// trace field is a valid value indicating the stack trace
// should not be set.
getOurStackTrace();
StackTraceElement[] oldStackTrace = stackTrace;
try {
if (stackTrace == null)
stackTrace = SentinelHolder.STACK_TRACE_SENTINEL;
s.defaultWriteObject();
} finally {
stackTrace = oldStackTrace;
}
}
首先获取stackTrace,然后把stackTrace备份到oldStackTrace中。如果stackTrace为null,也就是不可访问的状态,则把stackTrace设置为STACK_TRACE_SENTINEL。接下来将所有字段通过s.defaultWriteObject()序列化到对象流中。最后把stackTrace还原成备份的oldStackTrace。
为什么要将stackTrace=null转换成STACK_TRACE_SENTINEL呢?因为在更早的JDK版本,stackTrace不会参与序列化,所以更早的JDK在序列化的时候,stackTrace就是null;而在JDK8中,stackTrace在本地为null的时候,其实是代表不可访问,所以在序列化的时候需要用STACK_TRACE_SENTINEL来代替,并在反序列化的时候还原。这样就可以区分以上两种不同的语义,实现不同版本JDK的兼容。
Throwable的readObject方法和writeObject的实现思路也是类似的,不再做分析。
try-with-resource是从java7提供的语法糖,可以简化Closable对象的关闭。网上有很多资料,就不再介绍了。
如果异常e2是异常e1产生的底层原因,则可以这样抛出异常:
try{
...
}catch(LowLevelException e1){
HighLevelException e2 = new HighLevelException("high level异常", e1);
throw e2;
}
或者:
try{
...
}catch(LowLevelException e1){
HighLevelException e2 = new HighLevelException("high level异常");
e2.initCause(e1);
throw e2;
}
如果e1和e2没有直接关联,但是e2在catch或finally中直接抛出会覆盖e1,这种情况在catch中有复杂代码逻辑时是有可能发生的。这时可以这样解决:
try{
...
}catch(OneException e1){
try{
recordErrorToDatabase(e1);
}catch(SqlException e2){
e1.addSuppressed(e2);
}
throw e1;
}
try-with-resource也使用了类似的方法来防止原始异常被finally中的异常覆盖而丢失。
public static T doWithRetry(Callable callable, int retryTime, Class extends Throwable> expectedThrowableClass){
Objects.requireNonNull(callable, "callable can't be null");
if(retryTime < 0){
throw new IllegalArgumentException("retryTime can't be negative");
}
Throwable t = null;
int i = 0;
while(i <= retryTime){
try{
if(i > 0){
System.out.println("begin to retry, retry time: " + i);
}
return callable.call();
}catch (Throwable t1){
t = t1;
if(expectedThrowableClass.isInstance(t)){
++i;
}else {
throw new RuntimeException("unable to retry because the exception is not an expected type", t);
}
}
}
throw new RuntimeException("unable to retry because it's still failed after retry " + retryTime + " times", t);
}
try{
this.state = RUNNING;
// do something and Error occurs
this.state = SUCCESS;
}catch(Throwable e){
this.state = ERROR;
}finally{
...
}
如果资源回收依赖于我们自己设定的状态,此时用Exception捕获异常仍然不安全。因为运行时还是可能抛出Error的,我在之前的项目中出现过调用第三方jar包导致了AbstraceMethodError异常,从而导致catch代码块没有执行,this.state一直是RUNNING,导致上层模块无法回收线程,最终导致线程泄漏,服务无法响应的严重事故。
在这种情况下,catch中应该用Throwable去捕获异常。