JDK1.8异常处理——Throwable源码解析

目录

异常的生命周期

异常分类

Throwable的成员变量

detailMessage

stackTrace

suppressedExceptions

cause

异常打印

序列化/反序列化

应用

使用try-with-resource自动关闭资源

在一个异常中保留另一个异常

发生异常进行重试

使用Throwable捕获异常


异常的生命周期

        Throwable.java抽象了所有的异常,从异常的生命周期来看,可以分成:

  • 抛出一个异常:throw new Throwable
  • 将异常传递给调用方:throws
  • 捕获一个异常:catch

异常分类

(1)根据类的体系

  • Exception
  • Error

(2)根据是否在编译期间检查

  • unchecked exception
    • RuntimeException
    • Error
  • checked exception

Throwable的成员变量

detailMessage

private String detailMessage;

        这个字段用于存储异常的简要说明,可以通过e.getMessage()获得。

stackTrace

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的另外几个成员变量,也采用了这种设计思路。


suppressedExceptions

    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时,代表不可访问。


cause

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自动关闭资源

        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 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);
    }

使用Throwable捕获异常

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去捕获异常。

你可能感兴趣的:(java)