(Guava 译文系列)Throwables
Throwables
Guava 的Throwables
工具能经常简化与异常相关的操作。
传播
有些时候,当你捕获一个异常时,你想要将之再次抛出给下一个 try...catch
块。这种情况经常出现在遇到RuntimeException
和Error
的时候,这些异常不需要被捕获,但却仍会被
try...catch 块捕获,然而你并不想要这样做。
Guava 提供了许多工具来简化异常传播。例如: 1
2
3
4
5
6
7
8
9try {
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) |
传播RuntimeException 和
Error ,或将异常包装为RuntimeException 并以其它方式抛出 |
void propagateIfInstanceOf(Throwable, Class<X extends Exception>) throws X |
传播是X 的实例的异常 |
void propagateIfPossible(Throwable) |
传播是RuntimeException 或Error 的实例的异常 |
void propagateIfPossible(Throwable, Class<X extends Throwable>) throws X |
传播是RuntimeException 或Error 或X 的实例的异常 |
Throwables.propagate
的使用
模拟 Java 7 的 multi-catch 和 rethrow
通常如果你想要让异常传播至调用栈上一层,catch
块是完全不需要的。由于你不会从异常中恢复,所以可能并不应该对异常进行
log
或做其他的操作。你也许想要做一些清理工作,但是通常无论运行是否成功,清理工作都需要进行,所以需要在最后使用finally
块。然而,一个可以再次抛出异常的catch
块有时也有用:也许你想要在向上传播异常之前记录错误数,或者也许你只想在某些情况下才传播异常。
对于单个异常的情况,获取并再次抛出异常的过程直接简单。然而当存在多异常的情况时,问题会变得很糟:
1
2
3
4
5
6
7
8
9
10
11public void run() {
try {
delegate.run();
} catch (RuntimeException e) {
failures.increment();
throw e;
} catch (Error e) {
failures.increment();
throw e;
}
}1
2
3
4} catch (RuntimeException | Error e) {
failures.increment();
throw e;
}Throwable
类型的变量。
1
2
3
4} catch (Throwable t) {
failures.increment();
throw t;
}throw Throwables.propagate(t)
来替换throw t
。在有限的场景下,
Throwables.propagate
表现的与原先的代码行为一致。但是,采用Throwables.propagate
的方式,还能简单的包含其他隐藏的行为。特别要注意的是,以上模式只可用于RuntimeException
和Error
的情况。假如catch
块也可能会捕获检查类异常,则需要通过数个propagateIfInstanceOf
来确保行为正常,因为Throwables.propagate
无法直接传播检查类异常。
总而言之,使用propagate
的方式还 ok。当然在 Java 7
之后他就完全没必要了。在其他版本下,它能够减少一点点的重复代码,但是一个简单的方法抽取(Extract
Method)重构也能实现同样的效果。
另外,propagate
的用法很容易意外的包装检查类异常。
无需再将throws Throwable
转换为throws Exception
一些 API,尤其是 Java 反射类以及 JUnit (JUnit
大量使用了反射),声明方法会抛出Throwable
。与这些 API
交互会很痛苦,因为即使是最通用的 API
通常也只声明throws Exception
。Throwables.propagate
被一些知道他一定不会抛出Exception
和Error
的调用方使用。这里有一个定义Callable
来执行
JUnit 测试的例子: 1
2
3
4
5
6
7
8
9
10public 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
存在潜在的混淆,因为它不只是传播了给定的异常类型,还会传播RuntimeException
和Errors
。)
上述模式(或其变体例如throw new RuntimeException(t)
)在
Google 的代码库中出现了不下 30
次。(搜索'propagateIfPossible[^;]* Exception.class[)];'
。)其中只有一小部分采用了throw new RuntimeException(t)
的实现。也许我们想要一个throwWrappingWeirdThrowable
方法来进行Throwable
和Exception
的转换,但却采用了上述两行代码来代替,除非我们将propagateIfPossible
设为过时,否则也许并没有什么必要使用新方法。
####对Throwables.propagate
存在争议的用法
争议:将检查类异常转换为非检查异常
原则上将,非检查异常意味着
bug,检查异常意味着超出你控制范围的问题。事实上,连 JDK 有的时候也搞错了(或者至少对于某些方法,没有对所有人都正确的答案)。
结果就是,调用者有时需要在这两种异常之间做转换。
1 | try { |
1 | try { |
有的时候一些调用者会使用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 | } catch (Exception e) { |
然而,不容忽视的是将检查异常转换为非检查异常的通用实践,在有些时候是毋庸置疑的,然而更常见的是他多被用于避免处理合法的检查异常。这就引出了一个争论,即检查异常总体上是否是一个坏点子。我在这里并不想谈论这些。以上内容已经足以说明Throwables.propagate
并不存在用以鼓励
Java 用户忽略IOException
及类似异常的目的。
争议:从其他线程再抛出异常 1
2
3
4
5try {
return future.get();
} catch (ExecutionException e) {
throw Throwables.propagate(e.getCause());
}
这里需要考虑很多事情: 1. 上述 cause
可能是一个检查异常。可见上文”将检查异常转换为非检查异常”。但假如我们已知这个
task
不会抛出检查异常呢?(如果他是一个Runable
的结果。)按上述讨论,你可以捕获他,并抛出一个AssertionError
;propagate
可以提供稍多一点功能。尤其是对Future
,可以考虑Future.get
。
2. 上述 cause
可能不会抛出任何Exception
和Errors
。(好吧,这可能不是真的,但是如果你直接将之再抛出,则编译器确实会强制你考虑这种可能性。)可见上文:将throws Throwable
转换为
throws Exception
。 3. 上述 cause 可能是一个非检查异常或是
Error
。如果是,则他会被直接抛出。不幸的是,栈追踪信息会显示最初创建的线程的异常,而不是当前线程的传播该异常处。通常最好在异常链中包含两个线程的栈追踪信息,就像get
抛出的ExecutionException
一样。(这个问题实际上与propagate
无关;他与任何尝试在不同的线程再抛出异常的代码有关。)
因果链
Guava
为了能让研究一个异常的因果链变得简单一点,提供了三个有用的方法签名自解释的方法:
- Throwable getRootCause(Throwable)
- List<Throwable> getCausalChain(Throwable)
- String getStackTraceAsString(Throwable)