(Guava 译文系列)Throwables

Throwables

Guava 的Throwables工具能经常简化与异常相关的操作。

传播

有些时候,当你捕获一个异常时,你想要将之再次抛出给下一个 try...catch 块。这种情况经常出现在遇到RuntimeExceptionError的时候,这些异常不需要被捕获,但却仍会被 try...catch 块捕获,然而你并不想要这样做。

Guava 提供了许多工具来简化异常传播。例如:

1
2
3
4
5
6
7
8
9
try {
someMethodThatCouldThrowAnything();
} catch (IKnowWhatToDoWithThisException e) {
handle(e);
} catch (Throwable t) {
Throwables.propagateIfInstanceOf(t, IOException.class);
Throwables.propagateIfInstanceOf(t, SQLException.class);
throw Throwables.propagate(t);
}
这些方法每一个都会抛出异常,但抛出结果 - 例如 throw Throwables.propagate(t) - 对向编译器证明将抛出异常很有用。

以下为 Guava 提供的异常传播方法的小结:

Signature Explanation
RuntimeException propagate(Throwable) 传播RuntimeExceptionError,或将异常包装为RuntimeException并以其它方式抛出
void propagateIfInstanceOf(Throwable, Class<X extends Exception>) throws X 传播是X的实例的异常
void propagateIfPossible(Throwable) 传播是RuntimeExceptionError的实例的异常
void propagateIfPossible(Throwable, Class<X extends Throwable>) throws X 传播是RuntimeExceptionErrorX的实例的异常

Throwables.propagate的使用

模拟 Java 7 的 multi-catch 和 rethrow 通常如果你想要让异常传播至调用栈上一层,catch块是完全不需要的。由于你不会从异常中恢复,所以可能并不应该对异常进行 log 或做其他的操作。你也许想要做一些清理工作,但是通常无论运行是否成功,清理工作都需要进行,所以需要在最后使用finally块。然而,一个可以再次抛出异常的catch块有时也有用:也许你想要在向上传播异常之前记录错误数,或者也许你只想在某些情况下才传播异常。

对于单个异常的情况,获取并再次抛出异常的过程直接简单。然而当存在多异常的情况时,问题会变得很糟:

1
2
3
4
5
6
7
8
9
10
11
@Override public void run() {
try {
delegate.run();
} catch (RuntimeException e) {
failures.increment();
throw e;
} catch (Error e) {
failures.increment();
throw e;
}
}
Java 7 采用multicatch解决了此问题:
1
2
3
4
} catch (RuntimeException | Error e) {
failures.increment();
throw e;
}
然而非 Java 7 的用户被卡住了。他们想要采用以下方法来解决问题,然而编译器并不允许他们抛出一个Throwable类型的变量。
1
2
3
4
} catch (Throwable t) {
failures.increment();
throw t;
}
解决的办法就是用throw Throwables.propagate(t)来替换throw t。在有限的场景下, Throwables.propagate表现的与原先的代码行为一致。但是,采用Throwables.propagate的方式,还能简单的包含其他隐藏的行为。特别要注意的是,以上模式只可用于RuntimeExceptionError的情况。假如catch块也可能会捕获检查类异常,则需要通过数个propagateIfInstanceOf来确保行为正常,因为Throwables.propagate无法直接传播检查类异常。

总而言之,使用propagate的方式还 ok。当然在 Java 7 之后他就完全没必要了。在其他版本下,它能够减少一点点的重复代码,但是一个简单的方法抽取(Extract Method)重构也能实现同样的效果。

另外,propagate的用法很容易意外的包装检查类异常

无需再将throws Throwable转换为throws Exception 一些 API,尤其是 Java 反射类以及 JUnit (JUnit 大量使用了反射),声明方法会抛出Throwable。与这些 API 交互会很痛苦,因为即使是最通用的 API 通常也只声明throws ExceptionThrowables.propagate被一些知道他一定不会抛出ExceptionError的调用方使用。这里有一个定义Callable来执行 JUnit 测试的例子:

1
2
3
4
5
6
7
8
9
10
public Void call() throws Exception {
try {
FooTest.super.runTest();
} catch (Throwable t) {
Throwables.propagateIfPossible(t, Exception.class);
Throwables.propagate(t);
}

return null;
}
这里并不需要propagate(),因为第二行与throw new RuntimeException(t)相同。(题外话:这个例子也提醒到我propagateIfPossible存在潜在的混淆,因为它不只是传播了给定的异常类型,还会传播RuntimeExceptionErrors。)

上述模式(或其变体例如throw new RuntimeException(t))在 Google 的代码库中出现了不下 30 次。(搜索'propagateIfPossible[^;]* Exception.class[)];'。)其中只有一小部分采用了throw new RuntimeException(t)的实现。也许我们想要一个throwWrappingWeirdThrowable方法来进行ThrowableException的转换,但却采用了上述两行代码来代替,除非我们将propagateIfPossible设为过时,否则也许并没有什么必要使用新方法。

####对Throwables.propagate存在争议的用法 争议:将检查类异常转换为非检查异常 原则上将,非检查异常意味着 bug,检查异常意味着超出你控制范围的问题。事实上,连 JDK 有的时候错了(或者至少对于某些方法,没有对所有人都正确的答案)。

结果就是,调用者有时需要在这两种异常之间做转换。

1
2
3
4
5
try {
return Integer.parseInt(userInput);
} catch (NumberFormatException e) {
throw new InvalidInputException(e);
}
1
2
3
4
5
try {
return publicInterfaceMethod.invoke();
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}

有的时候一些调用者会使用Throwables.propagate。那么使用他有什么不好的地方呢?

一个主要的问题是这会使代码变得不够清晰。throw Throwables.propagate(ioException)是干什么的?throw new RuntimeException(ioException)是干什么的?这二者做着同样的事情,但后者显然更直截了当。前者引出了问题:"这到底是干什么的?他应该不只是包装了RuntimeException对吧?如果是的话,那干嘛还要包装一层呢?"

诚然,问题的一部分出在“propagate”本身就是一个模糊地命名。这是一种抛出未声明异常的方法吗?也许叫做“wrapIfChecked“可能会更好。但是即使调用了该方法,在已知的检查异常上调用它也没有任何好处。他甚至还可能存在一个额外的问题:也许相比RuntimeException,抛出IllegalArgumentException会更好。

我们有时也会看到propagate使用在也许只会抛出检查异常的地方。结果就是这与通常的方式相比更小一点,也更不直接一点:

1
2
3
4
5
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}

1
2
3
} catch (Exception e) {
throw Throwables.propagate(e);
}

然而,不容忽视的是将检查异常转换为非检查异常的通用实践,在有些时候是毋庸置疑的,然而更常见的是他多被用于避免处理合法的检查异常。这就引出了一个争论,即检查异常总体上是否是一个坏点子。我在这里并不想谈论这些。以上内容已经足以说明Throwables.propagate并不存在用以鼓励 Java 用户忽略IOException及类似异常的目的。

争议:从其他线程再抛出异常

1
2
3
4
5
try {
return future.get();
} catch (ExecutionException e) {
throw Throwables.propagate(e.getCause());
}

这里需要考虑很多事情: 1. 上述 cause 可能是一个检查异常。可见上文”将检查异常转换为非检查异常”。但假如我们已知这个 task 不会抛出检查异常呢?(如果他是一个Runable的结果。)按上述讨论,你可以捕获他,并抛出一个AssertionErrorpropagate可以提供稍多一点功能。尤其是对Future,可以考虑Future.get。 2. 上述 cause 可能不会抛出任何ExceptionErrors。(好吧,这可能不是真的,但是如果你直接将之再抛出,则编译器确实会强制你考虑这种可能性。)可见上文:将throws Throwable转换为 throws Exception。 3. 上述 cause 可能是一个非检查异常或是 Error。如果是,则他会被直接抛出。不幸的是,栈追踪信息会显示最初创建的线程的异常,而不是当前线程的传播该异常处。通常最好在异常链中包含两个线程的栈追踪信息,就像get抛出的ExecutionException一样。(这个问题实际上与propagate无关;他与任何尝试在不同的线程再抛出异常的代码有关。)

因果链

Guava 为了能让研究一个异常的因果链变得简单一点,提供了三个有用的方法签名自解释的方法: - Throwable getRootCause(Throwable) - List<Throwable> getCausalChain(Throwable) - String getStackTraceAsString(Throwable)