我们经常通过 try catch 代码块包住一段可能出现异常的代码,同时在 catch 块中打印异常信息,如下所示:
public static void main(String args[]) {try {a();} catch (HighLevelException e) {e.printStackTrace();}}
当然通常情况我们会使用日志框架,不会直接使用 e.printStackTrace() 方法打印异常栈,但是日志框架同样会输出整个异常栈的信息,如下所示:
logger.error("error message", e);
大部分情况下,我们是不需要查看整个异常栈的信息就可以定位问题的,而且大量的异常栈信息堆积到日志文件中,对硬盘的消耗较大。如果同步到 ELK 中,对日志的内容进行分析,对 ES 存储的消耗也会很大。
那怎么限制异常栈的行数呢?
首先,我们来分析 e.printStackTrace() 方法的源码,看看它是如何打印异常栈信息的。
printStackTrace() 方法在 java.lang.Throwable 类中,方法的调用链路如下所示:
java.lang.Throwable#printStackTrace() -> java.lang.Throwable#printStackTrace(java.io.PrintStream) -> java.lang.Throwable#printStackTrace(java.lang.Throwable.PrintStreamOrWriter)
我们来看真正执行打印的方法:
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 traces.println(this);StackTraceElement[] trace = getOurStackTrace();for (StackTraceElement traceElement : trace)s.println("\tat " + traceElement);// Print suppressed exceptions, if anyfor (Throwable se : getSuppressed())se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);// Print cause, if anyThrowable ourCause = getCause();if (ourCause != null)ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);}}public StackTraceElement[] getStackTrace() {return getOurStackTrace().clone();}private synchronized StackTraceElement[] getOurStackTrace() {// Initialize stack trace field with information from// backtrace if this is the first call to this methodif (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;}
通过上面的代码,可以知道 e.printStackTrace() 方法中打印的内容是 StackTraceElement[],而且在接下来会输出 suppressed exceptions 和 cause 信息。
getStackTrace() 已经提供了 public 方法,可以通过 e.getStackTrace() 方式直接获取,不需要通过反射调用下面的私有方法。
新建自定义日志帮助类,限制异常栈的打印行数。
在自定义类之前,先准备两个会输出异常信息的测试类,一个(ExceptionTest1)会输出 Caused by,一个(ExceptionTest2)会输出 Suppressed。
public class ExceptionTest1 {public static void main(String args[]) {try {a();} catch (HighLevelException e) {System.out.println(LoggerHelper.printTop10StackTrace(e));}}static void a() throws HighLevelException {try {b();} catch (MidLevelException e) {throw new HighLevelException(e);}}static void b() throws MidLevelException {c();}static void c() throws MidLevelException {try {d();} catch (LowLevelException e) {throw new MidLevelException(e);}}static void d() throws LowLevelException {e();}static void e() throws LowLevelException {throw new LowLevelException();}}class HighLevelException extends Exception {HighLevelException(Throwable cause) {super(cause);}}class MidLevelException extends Exception {MidLevelException(Throwable cause) {super(cause);}}class LowLevelException extends Exception {}
public class ExceptionTest2 {static class ResourceB implements AutoCloseable {public void read() throws Exception {throw new Exception("ResourceB read exception");}@Overridepublic void close() throws Exception {throw new Exception("ResourceB close exception");}}static class ResourceA implements AutoCloseable {public void read() throws Exception {throw new Exception("ResourceA read exception");}@Overridepublic void close() throws Exception {throw new Exception("ResourceA close exception");}}public static void test() throws Exception {try (ResourceA a = new ResourceA();ResourceB b = new ResourceB()) {a.read();b.read();} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) throws Exception {test();}}
自定义日志帮助类 - 1.0 版本:
import org.apache.commons.lang3.ArrayUtils;public class LoggerHelper {private static final String SEPARATOR = "\r\n";public static String printTop10StackTrace(Throwable e) {return printStackTrace(e, 20);}public static String printStackTrace(Throwable e, int maxLineCount) {StringBuilder sb = new StringBuilder(maxLineCount * 5);sb.append(e.toString());sb.append(SEPARATOR);StackTraceElement[] trace = e.getStackTrace();if (trace == null) {return e.toString();}int count = maxLineCount > trace.length ? trace.length : maxLineCount;for (int i = 0; i < count; i++) {sb.append("\tat ").append(trace[i]).append(SEPARATOR);}// Print suppressed exceptions, if anyThrowable[] suppressedExceptions = e.getSuppressed();if (ArrayUtils.isNotEmpty(suppressedExceptions)) {sb.append("\tSuppressed: ");for (Throwable suppressedException : suppressedExceptions) {sb.append(printStackTrace(suppressedException, maxLineCount));}}// Print cause, if anyThrowable cause = e.getCause();if (cause != null) {sb.append("Caused by: ");sb.append(printStackTrace(cause, maxLineCount));}return sb.toString();}}
在 ExceptionTest1 类中使用该帮助类,如下所示:
public static void main(String args[]) {try {a();} catch (HighLevelException e) {System.out.println(LoggerHelper.printTop10StackTrace(e));}}
打印的结果如下所示:
D:\tools\Java\jdk1.8.0_231\bin\java.exe "-javaagent:D:\tools\JetBrains\IntelliJ IDEA 2018.3.6\lib\idea_rt.jar=51125:D:\tools\JetBrains\IntelliJ IDEA 2018.3.6\bin" -Dfile.encoding=UTF-8 -classpath D:\tools\Java\jdk1.8.0_231\jre\lib\charsets.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\deploy.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\access-bridge-64.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\cldrdata.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\dnsns.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\jaccess.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\jfxrt.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\localedata.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\nashorn.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\sunec.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\sunjce_provider.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\sunmscapi.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\sunpkcs11.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\ext\zipfs.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\javaws.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\jce.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\jfr.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\jfxswt.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\jsse.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\management-agent.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\plugin.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\resources.jar;D:\tools\Java\jdk1.8.0_231\jre\lib\rt.jar;D:\projects\test\target\classes;D:\tools\apache-maven-3.2.1\repo\net\javacrumbs\json-unit\json-unit-assertj\2.14.0\json-unit-assertj-2.14.0.jar;D:\tools\apache-maven-3.2.1\repo\net\javacrumbs\json-unit\json-unit-core\2.14.0\json-unit-core-2.14.0.jar;D:\tools\apache-maven-3.2.1\repo\org\opentest4j\opentest4j\1.1.1\opentest4j-1.1.1.jar;D:\tools\apache-maven-3.2.1\repo\org\hamcrest\hamcrest-core\2.1\hamcrest-core-2.1.jar;D:\tools\apache-maven-3.2.1\repo\org\hamcrest\hamcrest\2.1\hamcrest-2.1.jar;D:\tools\apache-maven-3.2.1\repo\org\assertj\assertj-core\3.15.0\assertj-core-3.15.0.jar;D:\tools\apache-maven-3.2.1\repo\net\javacrumbs\json-unit\json-unit-json-path\2.14.0\json-unit-json-path-2.14.0.jar;D:\tools\apache-maven-3.2.1\repo\com\jayway\jsonpath\json-path\2.4.0\json-path-2.4.0.jar;D:\tools\apache-maven-3.2.1\repo\net\minidev\json-smart\2.3\json-smart-2.3.jar;D:\tools\apache-maven-3.2.1\repo\net\minidev\accessors-smart\1.2\accessors-smart-1.2.jar;D:\tools\apache-maven-3.2.1\repo\org\ow2\asm\asm\5.0.4\asm-5.0.4.jar;D:\tools\apache-maven-3.2.1\repo\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\tools\apache-maven-3.2.1\repo\com\fasterxml\jackson\core\jackson-core\2.10.3\jackson-core-2.10.3.jar;D:\tools\apache-maven-3.2.1\repo\com\fasterxml\jackson\core\jackson-annotations\2.10.3\jackson-annotations-2.10.3.jar;D:\tools\apache-maven-3.2.1\repo\com\fasterxml\jackson\core\jackson-databind\2.10.3\jackson-databind-2.10.3.jar;D:\tools\apache-maven-3.2.1\repo\com\fasterxml\jackson\dataformat\jackson-dataformat-cbor\2.10.3\jackson-dataformat-cbor-2.10.3.jar;D:\tools\apache-maven-3.2.1\repo\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;D:\tools\apache-maven-3.2.1\repo\commons-collections\commons-collections\3.2.2\commons-collections-3.2.2.jar;D:\tools\apache-maven-3.2.1\repo\com\google\guava\guava\29.0-jre\guava-29.0-jre.jar;D:\tools\apache-maven-3.2.1\repo\com\google\guava\failureaccess\1.0.1\failureaccess-1.0.1.jar;D:\tools\apache-maven-3.2.1\repo\com\google\guava\listenablefuture\9999.0-empty-to-avoid-conflict-with-guava\listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar;D:\tools\apache-maven-3.2.1\repo\com\google\code\findbugs\jsr305\3.0.2\jsr305-3.0.2.jar;D:\tools\apache-maven-3.2.1\repo\org\checkerframework\checker-qual\2.11.1\checker-qual-2.11.1.jar;D:\tools\apache-maven-3.2.1\repo\com\google\errorprone\error_prone_annotations\2.3.4\error_prone_annotations-2.3.4.jar;D:\tools\apache-maven-3.2.1\repo\com\google\j2objc\j2objc-annotations\1.3\j2objc-annotations-1.3.jar;D:\tools\apache-maven-3.2.1\repo\commons-beanutils\commons-beanutils\1.9.4\commons-beanutils-1.9.4.jar;D:\tools\apache-maven-3.2.1\repo\commons-logging\commons-logging\1.2\commons-logging-1.2.jar test4.ExceptionTest1test4.HighLevelException: test4.MidLevelException: test4.LowLevelExceptionat test4.ExceptionTest1.a(ExceptionTest1.java:16)at test4.ExceptionTest1.main(ExceptionTest1.java:6)Caused by: test4.MidLevelException: test4.LowLevelExceptionat test4.ExceptionTest1.c(ExceptionTest1.java:28)at test4.ExceptionTest1.b(ExceptionTest1.java:21)at test4.ExceptionTest1.a(ExceptionTest1.java:14)at test4.ExceptionTest1.main(ExceptionTest1.java:6)Caused by: test4.LowLevelExceptionat test4.ExceptionTest1.e(ExceptionTest1.java:37)at test4.ExceptionTest1.d(ExceptionTest1.java:33)at test4.ExceptionTest1.c(ExceptionTest1.java:26)at test4.ExceptionTest1.b(ExceptionTest1.java:21)at test4.ExceptionTest1.a(ExceptionTest1.java:14)at test4.ExceptionTest1.main(ExceptionTest1.java:6)Process finished with exit code 0
对比 e.printStackTrace(); 方法异常栈的数据,稍稍有点区别:
查看源码,发现在打印 cause 数据的时候使用的是 java.lang.Throwable#printEnclosedStackTrace 方法,在该方法中对比了上层的异常栈信息,不会重复打印上层已经输出的内容。
private void printEnclosedStackTrace(PrintStreamOrWriter s,StackTraceElement[] enclosingTrace,String caption,String prefix,Set<Throwable> 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 traceStackTraceElement[] 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 traces.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 anyfor (Throwable se : getSuppressed())se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION,prefix +"\t", dejaVu);// Print cause, if anyThrowable ourCause = getCause();if (ourCause != null)ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, prefix, dejaVu);}}
参考 printEnclosedStackTrace() 方法,实现 2.0 版本:
public class LoggerHelper {private static final String SEPARATOR = "\r\n";private static final String CAUSE_CAPTION = "Caused by: ";private static final String SUPPRESSED_CAPTION = "Suppressed: ";/*** 默认返回前10行异常栈信息** @param e* @return*/public static String printTop10StackTrace(Throwable e) {if (e == null) {return "";}return printStackTrace(e, 20);}public static String printStackTrace(Throwable e, int maxLineCount) {if (e == null || maxLineCount <= 0) {return "";}StringBuilder sb = new StringBuilder(maxLineCount * 10);sb.append(e.toString()).append(SEPARATOR);StackTraceElement[] trace = e.getStackTrace();if (trace == null) {return e.toString();}int count = maxLineCount > trace.length ? trace.length : maxLineCount;int framesInCommon = trace.length - count;for (int i = 0; i < count; i++) {sb.append("\tat ").append(trace[i]).append(SEPARATOR);}if (framesInCommon != 0) {sb.append("\t... ").append(framesInCommon).append(" more").append(SEPARATOR);}// Print suppressed exceptions, if anyThrowable[] suppressedExceptions = e.getSuppressed();if (ArrayUtils.isNotEmpty(suppressedExceptions)) {for (Throwable suppressedException : suppressedExceptions) {sb.append(printEnclosedStackTrace(suppressedException, maxLineCount, trace, SUPPRESSED_CAPTION, "\t"));}}// Print cause, if anyThrowable cause = e.getCause();if (cause != null) {sb.append(printEnclosedStackTrace(cause, maxLineCount, trace, CAUSE_CAPTION, ""));}return sb.toString();}private static String printEnclosedStackTrace(Throwable e, int maxLineCount, StackTraceElement[] enclosingTrace,String caption, String prefix) {StringBuilder sb = new StringBuilder(maxLineCount * 5);StackTraceElement[] trace = e.getStackTrace();int m = trace.length - 1;int n = enclosingTrace.length - 1;while (m >= 0 && n >= 0 && trace[m].equals(enclosingTrace[n])) {m--;n--;}int count = maxLineCount > (m + 1) ? (m + 1) : maxLineCount;int framesInCommon = trace.length - count;// Print our stack tracesb.append(prefix).append(caption).append(e.toString()).append(SEPARATOR);for (int i = 0; i < count; i++) {sb.append(prefix).append("\tat ").append(trace[i]).append(SEPARATOR);}if (framesInCommon != 0) {sb.append(prefix).append("\t... ").append(framesInCommon).append(" more").append(SEPARATOR);}// Print suppressed exceptions, if anyThrowable[] suppressedExceptions = e.getSuppressed();if (ArrayUtils.isNotEmpty(suppressedExceptions)) {for (Throwable suppressedException : suppressedExceptions) {sb.append(printEnclosedStackTrace(suppressedException, maxLineCount, trace, SUPPRESSED_CAPTION, prefix + "\t"));}}// Print cause, if anyThrowable cause = e.getCause();if (cause != null) {sb.append(printEnclosedStackTrace(cause, maxLineCount, trace, CAUSE_CAPTION, prefix));}return sb.toString();}}
上面的类基本实现了对所有层次的异常栈都进行了打印行数的限制,且不会重复打印上层已经输出的内容。
除了像上面那样在打印日志的时候手动指定每层异常栈的行数,也可以通过配置文件,设置一些默认异常可以打印全部异常栈信息,或者设置一些默认异常不打印异常栈信息。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/okpfav 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
