1. 前言
在查看 JAVA 应用抛出的异常堆栈以排查问题时,我们有时会看到所谓 suppressed exceptions,即被抑制的异常。理解 suppressed exceptions 的原理,对我们分析问题的底层真实原因大有裨益。所以本文分析总结下 Java 中的 suppressed exceptions。
2. suppressed exceptions 机制总结
- 简单来说,suppressed exceptions 是 JVM 中一个真实发生了的异常,但由于某些原因被 JVM 忽略/抑制了;
- 一个常见的异常被忽略/抑制的场景是 try-catch-finally 代码块:由于无论 try 代码块是否正常执行结束,finally 代码块都会执行,所以如果 try 代码块和 finally 代码块都抛出异常时,为在打印的异常堆栈中完整还原异常现场,代码中可以做特殊处理(具体的处理方式见后文),以将两个异常都打印,并标记 try 中的异常为 suppressed;(用户需要对异常代码做处理);
- 另一个常见的异常被忽略的场景是 try-with-resources 代码块:java7 引进了 try-with-resources 代码块和 AutoCloseable 接口来管理资源,当 try-with-resources 底层的业务逻辑代码执行完毕时,无论其执行是否正常结束,jvm 都会自动关闭 try 中指定的 AutoCloseable 资源,以避免资源泄露,如果业务逻辑代码的处理和 AutoCloseable 资源的关闭都发生了异常,此时 jvm 会将两个异常都打印,并标记关闭 AutoCloseable 资源触发的异常为try 中的异常为 suppressed;(用户不用做特殊处理);
- 所以,为有效利用 suppressed exceptions 机制妥善打印异常堆栈以辅助问题排查,从 Java 7 开始, 我们可以使用 Throwable 类的如下方法来处理 suppressed exceptions: 即 java.lang.Throwable#addSuppressed 和java.lang.Throwable#getSuppressed
- A suppressed exception is an exception that is thrown but somehow ignored;
- A common scenario for this is the try-catch-finally block: when the finally block throws an exception,any exception originally thrown in the try block is then suppressed;
- Another common scenario is the try-with-resources block:Java 7 introduced the try-with-resources construct and the AutoCloseable interface for resource management,when exception occurs both in the business processing and resource closing,it’s the exception thrown in the close method that’s suppressed;
- Starting with Java 7, we can now use two methods on the Throwable class to handle our suppressed exceptions: addSuppressed and getSuppressed.
3 suppressed exceptions 机制 细节- try-catch-finally 代码块
- 当 finally 代码块没有使用 java.lang.Throwable#addSuppressed 对异常进行特殊处理时,如果 try 代码块和 finally 代码块都抛出异常,打印的异常堆栈的示例如下,可以看到,没有打印try 中的异常,而仅仅打印了 finally 中的异常,此时用户显然无法轻易获知异常的真实原因;
java.lang.NullPointerException
at com.keep.bdata.SuppressedExceptionsDemo.demoExceptionWithNoSuppress(SuppressedExceptionsDemo.java:21)
at com.keep.bdata.SuppressedExceptionsDemo.givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException(SuppressedExceptionsDemo.java:12)
图片
- 当 finally 代码块使用 java.lang.Throwable#addSuppressed 对异常进行了特殊处理时,如果 try 代码块和 finally 代码块都抛出异常,打印的异常堆栈的示例如下,可以看到,try 中的异常和 finally 中的异常都被打印了,且 try 中的异常被标记为 suppressed exceptions, 如果用户理解 suppressed exceptions 的机制,通过这些异常堆栈,显然可以轻松获知异常的真实原因;
java.lang.NullPointerException
at com.keep.bdata.SuppressedExceptionsDemo.demoExceptionWithSuppressed(SuppressedExceptionsDemo.java:38)
at com.keep.bdata.SuppressedExceptionsDemo.givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException_withSuppressed(SuppressedExceptionsDemo.java:27)
Suppressed: java.io.FileNotFoundException: \non-existent-path\non-existent-file.txt (系统找不到指定的路径。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at com.keep.bdata.SuppressedExceptionsDemo.demoExceptionWithSuppressed(SuppressedExceptionsDemo.java:33)
图片
4 suppressed exceptions 机制 细节 - try-with-resources 代码块
- java7 引进了 try-with-resources 代码块和 AutoCloseable 接口来管理资源,当 try-with-resources 底层的业务逻辑代码执行完毕时,无论其执行是否正常结束,jvm 都会自动关闭 try 中指定的 AutoCloseable 资源,以避免资源泄露;
- 如果业务逻辑代码的处理和 AutoCloseable 资源的关闭都发生了异常,此时 jvm 会将两个异常都打印,并标记关闭 AutoCloseable 资源触发的异常为try 中的异常为 suppressed,打印的异常堆栈的示例如下,如果用户理解 suppressed exceptions 的机制,通过这些异常堆栈,显然可以轻松获知异常的真实原因;
- 注意这是jvm自己实现的,用户不需要对代码做特殊处理;
java.lang.IllegalArgumentException: Thrown from processSomething()
at com.keep.bdata.TryWithResourceDemo$ExceptionalResource.processSomething(TryWithResourceDemo.java:23)
at com.keep.bdata.TryWithResourceDemo.demoExceptionalResource(TryWithResourceDemo.java:17)
at com.keep.bdata.TryWithResourceDemo.givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException_suppressed(TryWithResourceDemo.java:12)
Suppressed: java.lang.NullPointerException: Thrown from close()
at com.keep.bdata.TryWithResourceDemo$ExceptionalResource.close(TryWithResourceDemo.java:28)
at com.keep.bdata.TryWithResourceDemo.demoExceptionalResource(TryWithResourceDemo.java:18)
图片
5 suppressed exceptions 机制完整示例代码
- suppressed exceptions 机制的完整示例代码如下(try-catch-finally ):
package com.keep.bdata;
import org.junit.jupiter.api.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
publicclass SuppressedExceptionsDemo {
@Test
public void givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException() throws IOException {
demoExceptionWithNoSuppress("/non-existent-path/non-existent-file.txt");
}
public static void demoExceptionWithNoSuppress(String filePath) throws IOException {
FileInputStream fileIn = null;
try {
fileIn = new FileInputStream(filePath);
} catch (FileNotFoundException e) {
thrownew IOException(e);
} finally {
fileIn.close();
}
}
@Test
public void givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException_withSuppressed() throws IOException{
demoExceptionWithSuppressed("/non-existent-path/non-existent-file.txt");
}
public static void demoExceptionWithSuppressed(String filePath) throws IOException {
Throwable firstException = null;
FileInputStream fileIn = null;
try {
fileIn = new FileInputStream(filePath);
} catch (IOException e) {
firstException = e;
} finally {
try {
fileIn.close();
} catch (NullPointerException npe) {
if (firstException != null) {
npe.addSuppressed(firstException);
}
throw npe;
}
}
}
}
- suppressed exceptions 机制的完整示例代码如下(try-with-resources 完整示例代码):
package com.keep.bdata;
import org.junit.jupiter.api.Test;
publicclass TryWithResourceDemo {
@Test
public void givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException_suppressed() throws Exception {
demoExceptionalResource();
}
public void demoExceptionalResource() throws Exception {
try (ExceptionalResource exceptionalResource = new ExceptionalResource()) {
exceptionalResource.processSomething();
}
}
class ExceptionalResource implements AutoCloseable {
public void processSomething() {
thrownew IllegalArgumentException("Thrown from processSomething()");
}
@Override
public void close() throws Exception {
thrownew NullPointerException("Thrown from close()");
}
}