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 仓库