1. 简介

在 Java 中处理嵌套异常是很常见的,它们可以帮助我们追踪错误的源头。

在处理这类异常时,我们有时希望知道导致异常的原始问题,以便应用程序能根据不同的异常做出不同的响应。这种机制在使用一些将原始异常包装成自己类型的框架时尤其有用。

在这篇简短的文章中,我们将展示如何使用纯 Java,以及 Apache Commons Lang 和 Google Guava 这类第三方库来获取异常的根本原因(root cause)。


2. 一个年龄计算器应用

我们写一个简单的年龄计算器应用,它接收一个 ISO 格式的日期字符串,然后计算出对应的年龄。在解析过程中,我们处理两种错误情况:格式错误的日期、未来日期。

首先定义两个自定义异常:

static class InvalidFormatException extends DateParseException {

    InvalidFormatException(String input, Throwable thr) {
        super("Invalid date format: " + input, thr);
    }
}

static class DateOutOfRangeException extends DateParseException {

    DateOutOfRangeException(String date) {
        super("Date out of range: " + date);
    }

}

这两个异常都继承自一个公共父类,以保持代码结构清晰:

static class DateParseException extends RuntimeException {

    DateParseException(String input) {
        super(input);
    }

    DateParseException(String input, Throwable thr) {
        super(input, thr);
    }
}

接下来,我们实现 AgeCalculator 类中的日期解析方法:

static class AgeCalculator {

    private static LocalDate parseDate(String birthDateAsString) {
        LocalDate birthDate;
        try {
            birthDate = LocalDate.parse(birthDateAsString);
        } catch (DateTimeParseException ex) {
            throw new InvalidFormatException(birthDateAsString, ex);
        }

        if (birthDate.isAfter(LocalDate.now())) {
            throw new DateOutOfRangeException(birthDateAsString);
        }

        return birthDate;
    }
}

当格式错误时,我们把 DateTimeParseException 包装成自定义的 InvalidFormatException

最后,添加一个公共方法,接收日期字符串并计算年龄:

public static int calculateAge(String birthDate) {
    if (birthDate == null || birthDate.isEmpty()) {
        throw new IllegalArgumentException();
    }

    try {
        return Period
          .between(parseDate(birthDate), LocalDate.now())
          .getYears();
    } catch (DateParseException ex) {
        throw new CalculationException(ex);
    }
}

这里我们再次包装异常,将其封装为 CalculationException

static class CalculationException extends RuntimeException {

    CalculationException(DateParseException ex) {
        super(ex);
    }
}

现在我们可以使用这个类来计算年龄了:

AgeCalculator.calculateAge("2019-10-01");

当出现异常时,我们也希望能快速定位到原始错误。继续往下看就知道怎么做了。


3. 使用纯 Java 查找根本原因

第一种方法是使用纯 Java 实现一个方法,循环查找异常的 cause,直到最底层:

public static Throwable findCauseUsingPlainJava(Throwable throwable) {
    Objects.requireNonNull(throwable);
    Throwable rootCause = throwable;
    while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
        rootCause = rootCause.getCause();
    }
    return rootCause;
}

✅ 这里加了个判断,防止出现循环引用导致死循环。

当我们传入非法格式的日期:

try {
    AgeCalculator.calculateAge("010102");
} catch (CalculationException ex) {
    assertTrue(findCauseUsingPlainJava(ex) instanceof DateTimeParseException);
}

此时 root cause 是 DateTimeParseException

如果传入的是未来日期:

try {
    AgeCalculator.calculateAge("2020-04-04");
} catch (CalculationException ex) {
    assertTrue(findCauseUsingPlainJava(ex) instanceof DateOutOfRangeException);
}

结果是 DateOutOfRangeException

甚至传入 null:

try {
    AgeCalculator.calculateAge(null);
} catch (Exception ex) {
    assertTrue(findCauseUsingPlainJava(ex) instanceof IllegalArgumentException);
}

✅ 此时返回的是 IllegalArgumentException


4. 使用 Apache Commons Lang 获取根本原因

Apache Commons Lang 提供了一个非常实用的类 ExceptionUtils,其中包含 getRootCause() 方法,可直接获取异常的 root cause。

示例代码如下:

try {
    AgeCalculator.calculateAge("010102");
} catch (CalculationException ex) {
    assertTrue(ExceptionUtils.getRootCause(ex) instanceof DateTimeParseException);
}

✅ 与前面方法结果一致,非常简洁。


5. 使用 Guava 获取根本原因

Guava 提供了 Throwables 工具类,同样有 getRootCause() 方法。

使用方式如下:

try {
    AgeCalculator.calculateAge("010102");
} catch (CalculationException ex) {
    assertTrue(Throwables.getRootCause(ex) instanceof DateTimeParseException);
}

✅ 效果一样,代码简单明了。


6. 总结

本文演示了在 Java 中如何处理嵌套异常,并编写了一个工具方法来定位异常的根本原因。

同时,我们也展示了使用 Apache Commons Lang 和 Guava 这两个流行库来简化 root cause 的查找过程。

✅ 推荐做法:

  • 如果项目中已有 Apache Commons Lang 或 Guava,建议直接使用其工具类。
  • 如果想避免引入第三方依赖,也可以自己实现一个简单的 root cause 查找方法。

⚠️ 注意:自定义异常时记得保留原始 cause,否则 root cause 无法正确追踪。

完整示例代码可参考:GitHub 仓库


原始标题:How to Find an Exception's Root Cause in Java