我们经常通过 try catch 代码块包住一段可能出现异常的代码,同时在 catch 块中打印异常信息,如下所示:

    1. public static void main(String args[]) {
    2. try {
    3. a();
    4. } catch (HighLevelException e) {
    5. e.printStackTrace();
    6. }
    7. }

    当然通常情况我们会使用日志框架,不会直接使用 e.printStackTrace() 方法打印异常栈,但是日志框架同样会输出整个异常栈的信息,如下所示:

    1. 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)

    我们来看真正执行打印的方法:

    1. private void printStackTrace(PrintStreamOrWriter s) {
    2. // Guard against malicious overrides of Throwable.equals by
    3. // using a Set with identity equality semantics.
    4. Set<Throwable> dejaVu =
    5. Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
    6. dejaVu.add(this);
    7. synchronized (s.lock()) {
    8. // Print our stack trace
    9. s.println(this);
    10. StackTraceElement[] trace = getOurStackTrace();
    11. for (StackTraceElement traceElement : trace)
    12. s.println("\tat " + traceElement);
    13. // Print suppressed exceptions, if any
    14. for (Throwable se : getSuppressed())
    15. se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
    16. // Print cause, if any
    17. Throwable ourCause = getCause();
    18. if (ourCause != null)
    19. ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
    20. }
    21. }
    22. public StackTraceElement[] getStackTrace() {
    23. return getOurStackTrace().clone();
    24. }
    25. private synchronized StackTraceElement[] getOurStackTrace() {
    26. // Initialize stack trace field with information from
    27. // backtrace if this is the first call to this method
    28. if (stackTrace == UNASSIGNED_STACK ||
    29. (stackTrace == null && backtrace != null) /* Out of protocol state */) {
    30. int depth = getStackTraceDepth();
    31. stackTrace = new StackTraceElement[depth];
    32. for (int i=0; i < depth; i++)
    33. stackTrace[i] = getStackTraceElement(i);
    34. } else if (stackTrace == null) {
    35. return UNASSIGNED_STACK;
    36. }
    37. return stackTrace;
    38. }

    通过上面的代码,可以知道 e.printStackTrace() 方法中打印的内容是 StackTraceElement[],而且在接下来会输出 suppressed exceptions 和 cause 信息。

    getStackTrace() 已经提供了 public 方法,可以通过 e.getStackTrace() 方式直接获取,不需要通过反射调用下面的私有方法。

    新建自定义日志帮助类,限制异常栈的打印行数。

    在自定义类之前,先准备两个会输出异常信息的测试类,一个(ExceptionTest1)会输出 Caused by,一个(ExceptionTest2)会输出 Suppressed。

    1. public class ExceptionTest1 {
    2. public static void main(String args[]) {
    3. try {
    4. a();
    5. } catch (HighLevelException e) {
    6. System.out.println(LoggerHelper.printTop10StackTrace(e));
    7. }
    8. }
    9. static void a() throws HighLevelException {
    10. try {
    11. b();
    12. } catch (MidLevelException e) {
    13. throw new HighLevelException(e);
    14. }
    15. }
    16. static void b() throws MidLevelException {
    17. c();
    18. }
    19. static void c() throws MidLevelException {
    20. try {
    21. d();
    22. } catch (LowLevelException e) {
    23. throw new MidLevelException(e);
    24. }
    25. }
    26. static void d() throws LowLevelException {
    27. e();
    28. }
    29. static void e() throws LowLevelException {
    30. throw new LowLevelException();
    31. }
    32. }
    33. class HighLevelException extends Exception {
    34. HighLevelException(Throwable cause) {
    35. super(cause);
    36. }
    37. }
    38. class MidLevelException extends Exception {
    39. MidLevelException(Throwable cause) {
    40. super(cause);
    41. }
    42. }
    43. class LowLevelException extends Exception {
    44. }
    1. public class ExceptionTest2 {
    2. static class ResourceB implements AutoCloseable {
    3. public void read() throws Exception {
    4. throw new Exception("ResourceB read exception");
    5. }
    6. @Override
    7. public void close() throws Exception {
    8. throw new Exception("ResourceB close exception");
    9. }
    10. }
    11. static class ResourceA implements AutoCloseable {
    12. public void read() throws Exception {
    13. throw new Exception("ResourceA read exception");
    14. }
    15. @Override
    16. public void close() throws Exception {
    17. throw new Exception("ResourceA close exception");
    18. }
    19. }
    20. public static void test() throws Exception {
    21. try (ResourceA a = new ResourceA();
    22. ResourceB b = new ResourceB()) {
    23. a.read();
    24. b.read();
    25. } catch (Exception e) {
    26. e.printStackTrace();
    27. }
    28. }
    29. public static void main(String[] args) throws Exception {
    30. test();
    31. }
    32. }

    自定义日志帮助类 - 1.0 版本:

    1. import org.apache.commons.lang3.ArrayUtils;
    2. public class LoggerHelper {
    3. private static final String SEPARATOR = "\r\n";
    4. public static String printTop10StackTrace(Throwable e) {
    5. return printStackTrace(e, 20);
    6. }
    7. public static String printStackTrace(Throwable e, int maxLineCount) {
    8. StringBuilder sb = new StringBuilder(maxLineCount * 5);
    9. sb.append(e.toString());
    10. sb.append(SEPARATOR);
    11. StackTraceElement[] trace = e.getStackTrace();
    12. if (trace == null) {
    13. return e.toString();
    14. }
    15. int count = maxLineCount > trace.length ? trace.length : maxLineCount;
    16. for (int i = 0; i < count; i++) {
    17. sb.append("\tat ").append(trace[i]).append(SEPARATOR);
    18. }
    19. // Print suppressed exceptions, if any
    20. Throwable[] suppressedExceptions = e.getSuppressed();
    21. if (ArrayUtils.isNotEmpty(suppressedExceptions)) {
    22. sb.append("\tSuppressed: ");
    23. for (Throwable suppressedException : suppressedExceptions) {
    24. sb.append(printStackTrace(suppressedException, maxLineCount));
    25. }
    26. }
    27. // Print cause, if any
    28. Throwable cause = e.getCause();
    29. if (cause != null) {
    30. sb.append("Caused by: ");
    31. sb.append(printStackTrace(cause, maxLineCount));
    32. }
    33. return sb.toString();
    34. }
    35. }

    在 ExceptionTest1 类中使用该帮助类,如下所示:

    1. public static void main(String args[]) {
    2. try {
    3. a();
    4. } catch (HighLevelException e) {
    5. System.out.println(LoggerHelper.printTop10StackTrace(e));
    6. }
    7. }

    打印的结果如下所示:

    1. 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
    2. test4.HighLevelException: test4.MidLevelException: test4.LowLevelException
    3. at test4.ExceptionTest1.a(ExceptionTest1.java:16)
    4. at test4.ExceptionTest1.main(ExceptionTest1.java:6)
    5. Caused by: test4.MidLevelException: test4.LowLevelException
    6. at test4.ExceptionTest1.c(ExceptionTest1.java:28)
    7. at test4.ExceptionTest1.b(ExceptionTest1.java:21)
    8. at test4.ExceptionTest1.a(ExceptionTest1.java:14)
    9. at test4.ExceptionTest1.main(ExceptionTest1.java:6)
    10. Caused by: test4.LowLevelException
    11. at test4.ExceptionTest1.e(ExceptionTest1.java:37)
    12. at test4.ExceptionTest1.d(ExceptionTest1.java:33)
    13. at test4.ExceptionTest1.c(ExceptionTest1.java:26)
    14. at test4.ExceptionTest1.b(ExceptionTest1.java:21)
    15. at test4.ExceptionTest1.a(ExceptionTest1.java:14)
    16. at test4.ExceptionTest1.main(ExceptionTest1.java:6)
    17. Process finished with exit code 0

    对比 e.printStackTrace(); 方法异常栈的数据,稍稍有点区别:
    image.png
    查看源码,发现在打印 cause 数据的时候使用的是 java.lang.Throwable#printEnclosedStackTrace 方法,在该方法中对比了上层的异常栈信息,不会重复打印上层已经输出的内容。

    1. private void printEnclosedStackTrace(PrintStreamOrWriter s,
    2. StackTraceElement[] enclosingTrace,
    3. String caption,
    4. String prefix,
    5. Set<Throwable> dejaVu) {
    6. assert Thread.holdsLock(s.lock());
    7. if (dejaVu.contains(this)) {
    8. s.println("\t[CIRCULAR REFERENCE:" + this + "]");
    9. } else {
    10. dejaVu.add(this);
    11. // Compute number of frames in common between this and enclosing trace
    12. StackTraceElement[] trace = getOurStackTrace();
    13. int m = trace.length - 1;
    14. int n = enclosingTrace.length - 1;
    15. while (m >= 0 && n >=0 && trace[m].equals(enclosingTrace[n])) {
    16. m--; n--;
    17. }
    18. int framesInCommon = trace.length - 1 - m;
    19. // Print our stack trace
    20. s.println(prefix + caption + this);
    21. for (int i = 0; i <= m; i++)
    22. s.println(prefix + "\tat " + trace[i]);
    23. if (framesInCommon != 0)
    24. s.println(prefix + "\t... " + framesInCommon + " more");
    25. // Print suppressed exceptions, if any
    26. for (Throwable se : getSuppressed())
    27. se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION,
    28. prefix +"\t", dejaVu);
    29. // Print cause, if any
    30. Throwable ourCause = getCause();
    31. if (ourCause != null)
    32. ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, prefix, dejaVu);
    33. }
    34. }

    参考 printEnclosedStackTrace() 方法,实现 2.0 版本:

    1. public class LoggerHelper {
    2. private static final String SEPARATOR = "\r\n";
    3. private static final String CAUSE_CAPTION = "Caused by: ";
    4. private static final String SUPPRESSED_CAPTION = "Suppressed: ";
    5. /**
    6. * 默认返回前10行异常栈信息
    7. *
    8. * @param e
    9. * @return
    10. */
    11. public static String printTop10StackTrace(Throwable e) {
    12. if (e == null) {
    13. return "";
    14. }
    15. return printStackTrace(e, 20);
    16. }
    17. public static String printStackTrace(Throwable e, int maxLineCount) {
    18. if (e == null || maxLineCount <= 0) {
    19. return "";
    20. }
    21. StringBuilder sb = new StringBuilder(maxLineCount * 10);
    22. sb.append(e.toString()).append(SEPARATOR);
    23. StackTraceElement[] trace = e.getStackTrace();
    24. if (trace == null) {
    25. return e.toString();
    26. }
    27. int count = maxLineCount > trace.length ? trace.length : maxLineCount;
    28. int framesInCommon = trace.length - count;
    29. for (int i = 0; i < count; i++) {
    30. sb.append("\tat ").append(trace[i]).append(SEPARATOR);
    31. }
    32. if (framesInCommon != 0) {
    33. sb.append("\t... ").append(framesInCommon).append(" more").append(SEPARATOR);
    34. }
    35. // Print suppressed exceptions, if any
    36. Throwable[] suppressedExceptions = e.getSuppressed();
    37. if (ArrayUtils.isNotEmpty(suppressedExceptions)) {
    38. for (Throwable suppressedException : suppressedExceptions) {
    39. sb.append(printEnclosedStackTrace(suppressedException, maxLineCount, trace, SUPPRESSED_CAPTION, "\t"));
    40. }
    41. }
    42. // Print cause, if any
    43. Throwable cause = e.getCause();
    44. if (cause != null) {
    45. sb.append(printEnclosedStackTrace(cause, maxLineCount, trace, CAUSE_CAPTION, ""));
    46. }
    47. return sb.toString();
    48. }
    49. private static String printEnclosedStackTrace(Throwable e, int maxLineCount, StackTraceElement[] enclosingTrace,
    50. String caption, String prefix) {
    51. StringBuilder sb = new StringBuilder(maxLineCount * 5);
    52. StackTraceElement[] trace = e.getStackTrace();
    53. int m = trace.length - 1;
    54. int n = enclosingTrace.length - 1;
    55. while (m >= 0 && n >= 0 && trace[m].equals(enclosingTrace[n])) {
    56. m--;
    57. n--;
    58. }
    59. int count = maxLineCount > (m + 1) ? (m + 1) : maxLineCount;
    60. int framesInCommon = trace.length - count;
    61. // Print our stack trace
    62. sb.append(prefix).append(caption).append(e.toString()).append(SEPARATOR);
    63. for (int i = 0; i < count; i++) {
    64. sb.append(prefix).append("\tat ").append(trace[i]).append(SEPARATOR);
    65. }
    66. if (framesInCommon != 0) {
    67. sb.append(prefix).append("\t... ").append(framesInCommon).append(" more").append(SEPARATOR);
    68. }
    69. // Print suppressed exceptions, if any
    70. Throwable[] suppressedExceptions = e.getSuppressed();
    71. if (ArrayUtils.isNotEmpty(suppressedExceptions)) {
    72. for (Throwable suppressedException : suppressedExceptions) {
    73. sb.append(printEnclosedStackTrace(suppressedException, maxLineCount, trace, SUPPRESSED_CAPTION, prefix + "\t"));
    74. }
    75. }
    76. // Print cause, if any
    77. Throwable cause = e.getCause();
    78. if (cause != null) {
    79. sb.append(printEnclosedStackTrace(cause, maxLineCount, trace, CAUSE_CAPTION, prefix));
    80. }
    81. return sb.toString();
    82. }
    83. }

    上面的类基本实现了对所有层次的异常栈都进行了打印行数的限制,且不会重复打印上层已经输出的内容。

    除了像上面那样在打印日志的时候手动指定每层异常栈的行数,也可以通过配置文件,设置一些默认异常可以打印全部异常栈信息,或者设置一些默认异常不打印异常栈信息。

    作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/okpfav 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。