如何最好地检查嵌套异常中某个特定异常类型是否是导致异常的原因(或原因之一)?

41

我正在编写一些JUnit测试,验证是否抛出了MyCustomException类型的异常。 然而,这种异常被其他异常包装了多次,例如在InvocationTargetException中被包装,然后又被包装在RuntimeException中。

如何最好地确定MyCustomException是否引起了实际捕获的异常?我想做类似于这样的事情(请参见下划线部分):


try {
    doSomethingPotentiallyExceptional();
    fail("Expected an exception.");
} catch (RuntimeException e) {
     if (!e.</code>wasCausedBy<code>(MyCustomException.class)
        fail("Expected a different kind of exception.");
}

我想避免在几个“层”深处调用getCause(),以及类似的丑陋的解决方法。 有更好的方式吗?

显然,Spring有一个NestedRuntimeException.contains(Class),它可以实现我的需求 - 但我没有使用Spring。

9个回答

56
如果您正在使用Apache Commons Lang,则可以使用以下内容:
(1)当原因应该完全是指定类型时
if (ExceptionUtils.indexOfThrowable(exception, ExpectedException.class) != -1) {
    // exception is or has a cause of type ExpectedException.class
}

(2) 当原因应为指定类型或其子类类型之一时

if (ExceptionUtils.indexOfType(exception, ExpectedException.class) != -1) {
    // exception is or has a cause of type ExpectedException.class or its subclass
}

2
那么我们如何返回我们期望的异常原因呢? - Tharindu Sathischandra
2
ExceptionUtils.hasCause(e, ExpectedException.class) - Gabriel

34

为什么要避免使用getCause方法呢?当然,你可以自己编写一个方法来执行此任务,例如:

public static boolean isCause(
    Class<? extends Throwable> expected,
    Throwable exc
) {
   return expected.isInstance(exc) || (
       exc != null && isCause(expected, exc.getCause())
   );
}

5
请注意,如果原因有一个循环(在某些情况下,如EclipseLink DB异常),此算法可能会导致无限循环。Apache Commons Lang ExceptionUtils :: getRootCause 处理了这种情况,所以Patrick的答案中的 indexOfThrowable 可能也是如此。 - DavidS
@DavidS 不是无限循环 - 它会快速抛出 StackOverflowError。如果你有这样的因果关系崩溃,那么你就有更大的问题(可能尝试重用异常对象,即使它们是奇怪的可变对象)。 - Tom Hawtin - tackline
4
然后就出现了一个StackOverflowError,谢谢。不过,这不是我编写的代码的问题;这是一些常见库的问题,包括一些Oracle jdbc驱动程序。这是一个非常普遍的问题,以至于Apache Commons选择在getRootCause中处理它,Oracle选择在printStackTrace中处理它,而Guava则考虑在Throwables中处理它。我的评论是为了警告这个问题,而不是推荐如何构造异常。 - DavidS
@DavidS 我很失望图书馆已经带有预定命运悖论。有趣的是,OpenJDK的printStackTace源代码提出了一些关于防范恶意异常的建议,但在多个方面都失败了。P4 bug - 我更担心图书馆而不是试图处理每一个可能的DoS bug。容易受到这种攻击的代码是无处不在的 - 比如使用equals - Tom Hawtin - tackline
1
我也见过在纯JDK的NoClassDefFound错误中出现无限循环(由于静态初始化块期间触发了另一个异常)。 - Joshua Goldberg
if (e == null) return null; if (expected.isInstance(e)) { return e; } return getCauseException(expected, e.getCause()); }``` 用于获取错误原因。 - Tharindu Sathischandra

6
我认为你没有其他选择,只能通过调用getCause方法来获取异常的根本原因。如果你查看Spring NestedRuntimeException源代码,你会发现它就是这样实现的。

2

模仿是最真诚的赞美。根据源代码的快速检查,这正是NestedRuntimeException所做的:

/**
 * Check whether this exception contains an exception of the given type:
 * either it is of the given class itself or it contains a nested cause
 * of the given type.
 * @param exType the exception type to look for
 * @return whether there is a nested exception of the specified type
 */
public boolean contains(Class exType) {
    if (exType == null) {
        return false;
    }
    if (exType.isInstance(this)) {
        return true;
    }
    Throwable cause = getCause();
    if (cause == this) {
        return false;
    }
    if (cause instanceof NestedRuntimeException) {
        return ((NestedRuntimeException) cause).contains(exType);
    }
    else {
        while (cause != null) {
            if (exType.isInstance(cause)) {
                return true;
            }
            if (cause.getCause() == cause) {
                break;
            }
            cause = cause.getCause();
        }
        return false;
    }
}

注意:上述代码是截至2009年3月4日的代码,如果您真正想知道Spring现在正在做什么,您应该研究当前存在的代码(无论何时)。


2

根据Patrick Boos的回答:

indexOfThrowable:在异常链中返回第一个与指定类(完全相同)匹配的Throwable的(从零开始的)索引。 指定类的子类不匹配

if (ExceptionUtils.indexOfThrowable(e, clazz) != -1) {
    // your code
}

indexOfType: 返回异常链中第一个与指定类或子类匹配的Throwable(从零开始计算的索引)。指定类的子类也会匹配

if (ExceptionUtils.indexOfType(e, clazz) != -1) {
    // your code
}

Java 8中多类型的示例:

Class<? extends Throwable>[] classes = {...}
boolean match = Arrays.stream(classes)
            .anyMatch(clazz -> ExceptionUtils.indexOfType(e, clazz) != -1);

2
您可以使用Guava来完成此操作:
FluentIterable.from(Throwables.getCausalChain(e))
                        .filter(Predicates.instanceOf(ConstraintViolationException.class))
                        .first()
                        .isPresent();

我认为在某些情况下,这也可能导致无限循环,就像其他答案一样。例如,我曾经在NoClassDefFound异常的getCause()中看到过这样的链。要避免这种情况需要像Spring中@BobCross所提供的答案中那样进行检查,即cause.getCause() == cause。 - Joshua Goldberg
1
@JoshuaGoldberg 这个在2017年发布的23.0版本中已经安全免受无限循环的影响,参见guava#2866。更具体地说,它会在这种情况下抛出IllegalArgumentException - M. Justin
@M.Justin,当链式递归并且getCausalChain抛出IllegalArgumentException时,它是否会防止其被用于检查原因链中的特定异常类型? - Joshua Goldberg
1
正确,如果有循环,您不能使用此方法来检查特定的异常。在这种情况下,它所做的是保护您免受由于无限循环而导致代码永远阻塞的影响。 - M. Justin

1

嗯,我认为没有办法在不调用getCause()的情况下完成这个操作。如果你认为这很丑陋,可以实现一个工具类来完成这个操作:

public class ExceptionUtils {
     public static boolean wasCausedBy(Throwable e, Class<? extends Throwable>) {
         // call getCause() until it returns null or finds the exception
     }
}

1
你可以使用Apache Commons Lang库中的ExceptionUtils.hasCause(ex, type)

请注意,hasCause在Apache的ExceptionUtils中。(还有一些其他包含ExceptionUtils类的包。) - Joshua Goldberg
@JoshuaGoldberg,我已更新答案以指示其位置。 - M. Justin

0
如果感兴趣的异常确实是“根本”原因,assertj是另一个可以找到getRootCause检查的地方,尽管从今天的源代码来看,它似乎存在其他答案中讨论的可能无限循环问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接