天涯共此时
在了解异常抑制之前,先来看看Throwable的addSuppressed()方法的注释(机翻)
将指定的异常附加到为了传递此异常而被抑制的异常。此方法是线程安全的,通常由try-with-resources语句(自动和隐式)调用。
除非通过构造函数禁用抑制行为,否则将启用抑制行为。禁用抑制后,此方法除了验证其参数外不执行其他操作。
请注意,当一个异常导致另一个异常时,通常会捕获第一个异常,然后作为响应抛出第二个异常。换句话说,两个例外之间存在因果关系。相反,在某些情况下,可能在同级代码块中引发两个独立的异常,特别是在try-with-resources语句的try块和编译器生成的finally块中,这两个异常会关闭资源。在这些情况下,只能传播所引发的异常之一。在try-with-resources语句中,当有两个这样的异常时,将传播来自try块的异常,并将finally块的异常添加到由try块的异常抑制的异常列表中。当异常展开堆栈时,它可以累积多个受抑制的异常。
异常可能抑制了异常,但也可能是由另一个异常引起的。在创建异常时,从语义上知道异常是否有原因,这与异常是否会抑制其他异常不同,后者通常仅在引发异常后才能确定。
不看也无所谓,看一下小demo就能理解了。
我们在对异常进行处理的时候,有时候在finally和catch中也会出现异常,还需要在catch或者finally中再对新的异常进行处理。可是在catch或者finally中抛出的异常会把try代码块中的异常给抑制调。但我们最需要的异常信息其实是try代码块中抛出的异常。
比如下面这个/zero的异常
@Test
public void test1() {
int a;
try {
a = 3 / 0;
System.out.println(a);
} catch (ArithmeticException zero) {
zero.printStackTrace();
}
}
java.lang.ArithmeticException: / by zero
at com.springlearn.qiyan.base.ThrowTest.test1(ThrowTest.java:41)
我们想要在catch中处理调这个可能为0的情况,给他一个新的被除数x,现在有一个新的getX()方法去获取x。这个getX()也并不靠谱,他可能会返回null,所以我们在catch代码块中再去捕获这个新的空指针异常。
public Integer getX(){
Random random = new Random();
boolean b = random.nextBoolean();
if(b){
return null;
}else{
return 1;
}
}
@Test
public void test() {
int a;
Integer x = getX();
try {
a = 3 / 0;
System.out.println(a);
} catch (ArithmeticException zero) {
try {
a = 3 / x;
System.out.println(a);
} catch (NullPointerException nullPoint) {
nullPoint.printStackTrace();
}
}
}
当getX()返回null的时候:返回的异常是NullPointerException,而不是外层的ArithmeticException
java.lang.NullPointerException
at com.springlearn.qiyan.base.ThrowTest.test(ThrowTest.java:26)
但是理想的状态应该是在外层的try代码块中实现对a的运算,即使出错,我们也应该去拿ArithmeticException去定位异常,空指针异常可能并不是我们想要捕获的。
这时候就可以使用Throwable中的addSuppressed方法去实现对两个异常的捕获
@Test
public void test3() {
int a;
Integer x = getX();
try {
a = 3 / 0;
System.out.println(a);
} catch (ArithmeticException zero) {
try {
a = 3 / x;
System.out.println(a);
} catch (NullPointerException nullPoint) {
//看这里
zero.addSuppressed(nullPoint);
zero.printStackTrace();
}
}
}
java.lang.ArithmeticException: / by zero
at com.springlearn.qiyan.base.ThrowTest.test3(ThrowTest.java:85)
Suppressed: java.lang.NullPointerException
at com.springlearn.qiyan.base.ThrowTest.test3(ThrowTest.java:89)
可以看到两个异常都被打印出来了。
这种catch或者finally中还要嵌套异常捕获的现象在对文件的处理中很常见:就比如说在finally中对外部资源进行关闭
@Test
public void test() {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("file.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这样写非常繁琐,jdk7中也提供了对try-with-resource语法去简化这样复杂的代码
@Test
public void test() {
try (FileInputStream inputStream = new FileInputStream("file.txt")) {
System.out.println(inputStream.read());
} catch (IOException e) {
e.printStackTrace();
}
}
看一下编译后的class文件
@Test
public void test() {
try {
FileInputStream inputStream = new FileInputStream("file.txt");
Throwable var2 = null;
try {
System.out.println(inputStream.read());
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (inputStream != null) {
if (var2 != null) {
try {
inputStream.close();
} catch (Throwable var11) {
//看这里
var2.addSuppressed(var11);
}
} else {
inputStream.close();
}
}
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
可以看到try-with-resource也是对异常进行了addSuppressed()操作
看完了demo,我们来看一下Throwable中的addSuppressed方法
首先我们先来看一下这个方法用到的几个集合和常量
//用来标识是一个被抑制的异常
private static final String SUPPRESSED_CAPTION = "Suppressed: ";
//定义一个大小为0且不可变的集合
private static final List<Throwable> SUPPRESSED_SENTINEL =
Collections.unmodifiableList(new ArrayList<Throwable>(0));
//使suppressedExceptions指向上述那个不可变的集合
private List<Throwable> suppressedExceptions = SUPPRESSED_SENTINEL;
代码:
public final synchronized void addSuppressed(Throwable exception) {
//不能抑制自身
if (exception == this)
throw new IllegalArgumentException(SELF_SUPPRESSION_MESSAGE, exception);
//被抑制的异常不能为空
if (exception == null)
throw new NullPointerException(NULL_CAUSE_MESSAGE);
//Thorwable的构造方法中可能会把suppressedExceptions设置为null,当为null时说明不启用异常抑制
if (suppressedExceptions == null) // Suppressed exceptions not recorded
return;
//如果是大小为0的不可变的集合,就为suppressedExceptions重新定义为大小为1的ArrayList,并把被抑制的异常加入
if (suppressedExceptions == SUPPRESSED_SENTINEL)
suppressedExceptions = new ArrayList<>(1);
suppressedExceptions.add(exception);
}
这个用来记录被抑制的异常的suppressedExceptions集合使用起来也很简单,只是单纯的遍历
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
//看这里,其实就是遍历打印,getSuppressed():返回所有被try-with-resources语句抑制的异常的数组
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);
}
}
最后我们看一下Spring的bean加载过程中关于异常抑制的地方。在DefaultSingletonBeanRegistry类的getSingleton方法中(只看异常抑制相关的地方)
//用于存储被抑制的异常
@Nullable
private Set<Exception> suppressedExceptions;
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
···
//判断异常抑制的集合是否为空
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
//如果为空,声明为新的集合
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<>();
}
try {
//方法逻辑
···
} catch (IllegalStateException ex) {
···
} catch (BeanCreationException ex) {
//遍历异常抑制集合,调用addRelatedCause方法,addRelatedCause与Throwable中的addSuppressed方法相似,就是判空与加入
if (recordSuppressedExceptions) {
for (Exception suppressedException : this.suppressedExceptions) {
ex.addRelatedCause(suppressedException);
}
}
//抛出
throw ex;
} finally {
···
}
···
}