我们经常通过 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 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);
}
}
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 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;
}
通过上面的代码,可以知道 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");
}
@Override
public 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");
}
@Override
public 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 any
Throwable[] suppressedExceptions = e.getSuppressed();
if (ArrayUtils.isNotEmpty(suppressedExceptions)) {
sb.append("\tSuppressed: ");
for (Throwable suppressedException : suppressedExceptions) {
sb.append(printStackTrace(suppressedException, maxLineCount));
}
}
// Print cause, if any
Throwable 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.ExceptionTest1
test4.HighLevelException: test4.MidLevelException: test4.LowLevelException
at test4.ExceptionTest1.a(ExceptionTest1.java:16)
at test4.ExceptionTest1.main(ExceptionTest1.java:6)
Caused by: test4.MidLevelException: test4.LowLevelException
at 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.LowLevelException
at 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 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);
}
}
参考 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 any
Throwable[] suppressedExceptions = e.getSuppressed();
if (ArrayUtils.isNotEmpty(suppressedExceptions)) {
for (Throwable suppressedException : suppressedExceptions) {
sb.append(printEnclosedStackTrace(suppressedException, maxLineCount, trace, SUPPRESSED_CAPTION, "\t"));
}
}
// Print cause, if any
Throwable 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 trace
sb.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 any
Throwable[] suppressedExceptions = e.getSuppressed();
if (ArrayUtils.isNotEmpty(suppressedExceptions)) {
for (Throwable suppressedException : suppressedExceptions) {
sb.append(printEnclosedStackTrace(suppressedException, maxLineCount, trace, SUPPRESSED_CAPTION, prefix + "\t"));
}
}
// Print cause, if any
Throwable 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 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。