天天看點

[Google Guava] 1.5-Throwables:簡化異常和錯誤的傳播與檢查

有時候,你會想把捕獲到的異常再次抛出。這種情況通常發生在error或runtimeexception被捕獲的時候,你沒想捕獲它們,但是聲明捕獲throwable和exception的時候,也包括了了error或runtimeexception。guava提供了若幹方法,來判斷異常類型并且重新傳播異常。例如:

<code>1</code>

<code>try</code> <code>{</code>

<code>2</code>

<code>    </code><code>somemethodthatcouldthrowanything();</code>

<code>3</code>

<code>} </code><code>catch</code> <code>(iknowwhattodowiththisexception e) {</code>

<code>4</code>

<code>    </code><code>handle(e);</code>

<code>5</code>

<code>} </code><code>catch</code> <code>(throwable t) {</code>

<code>6</code>

<code>    </code><code>throwables.propagateifinstanceof(t, ioexception.</code><code>class</code><code>);</code>

<code>7</code>

<code>    </code><code>throwables.propagateifinstanceof(t, sqlexception.</code><code>class</code><code>);</code>

<code>8</code>

<code>    </code><code>throw</code> <code>throwables.propagate(t);</code>

<code>9</code>

<code>}</code>

所有這些方法都會自己決定是否要抛出異常,但也能直接抛出方法傳回的結果——例如,throw throwables.propagate(t);—— 這樣可以向編譯器聲明這裡一定會抛出異常。

guava中的異常傳播方法簡要列舉如下:

<a href="http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/throwables.html#propagate(java.lang.throwable)">runtimeexception   propagate(throwable)</a>

如果throwable是error或runtimeexception,直接抛出;否則把throwable包裝成runtimeexception抛出。傳回類型是runtimeexception,是以你可以像上面說的那樣寫成throw throwables.propagate(t),java編譯器會意識到這行代碼保證抛出異常。

<a href="http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/throwables.html#propagateifinstanceof(java.lang.throwable,%20java.lang.class)">void propagateifinstanceof( throwable, class&lt;x extends   exception&gt;) throws x</a>

throwable類型為x才抛出

<a href="http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/throwables.html#propagateifpossible(java.lang.throwable)">void propagateifpossible( throwable)</a>

throwable類型為error或runtimeexception才抛出

<a href="http://goo.gl/pgdjc">void   propagateifpossible( throwable, class&lt;x extends throwable&gt;) throws x</a>

throwable類型為x, error或runtimeexception才抛出

通常來說,如果調用者想讓異常傳播到棧頂,他不需要寫任何catch代碼塊。因為他不打算從異常中恢複,他可能就不應該記錄異常,或者有其他的動作。他可能是想做一些清理工作,但通常來說,無論操作是否成功,清理工作都要進行,是以清理工作可能會放在finallly代碼塊中。但有時候,捕獲異常然後再抛出也是有用的:也許調用者想要在異常傳播之前統計失敗的次數,或者有條件地傳播異常。

當隻對一種異常進行捕獲和再抛出時,代碼可能還是簡單明了的。但當多種異常需要處理時,卻可能變得一團糟:

<code>01</code>

<code>@override</code> <code>public</code> <code>void</code> <code>run() {</code>

<code>02</code>

<code>    </code><code>try</code> <code>{</code>

<code>03</code>

<code>        </code><code>delegate.run();</code>

<code>04</code>

<code>    </code><code>} </code><code>catch</code> <code>(runtimeexception e) {</code>

<code>05</code>

<code>        </code><code>failures.increment();</code>

<code>06</code>

<code>        </code><code>throw</code> <code>e;</code>

<code>07</code>

<code>    </code><code>}</code><code>catch</code> <code>(error e) {</code>

<code>08</code>

<code>09</code>

<code>10</code>

<code>    </code><code>}</code>

<code>11</code>

java7用多重捕獲解決了這個問題:

<code>} </code><code>catch</code> <code>(runtimeexception | error e) {</code>

<code>    </code><code>failures.increment();</code>

<code>    </code><code>throw</code> <code>e;</code>

非java7使用者卻受困于這個問題。他們想要寫如下代碼來統計所有異常,但是編譯器不允許他們抛出throwable(譯者注:這種寫法把原本是error或runtimeexception類型的異常修改成了throwable,是以調用者不得不修改方法簽名):

<code>    </code><code>throw</code> <code>t;</code>

解決辦法是用throw throwables.propagate(t)替換throw t。在限定情況下(捕獲error和runtimeexception),throwables.propagate和原始代碼有相同行為。然而,用throwables.propagate也很容易寫出有其他隐藏行為的代碼。尤其要注意的是,這個方案隻适用于處理runtimeexception 或error。如果catch塊捕獲了受檢異常,你需要調用propagateifinstanceof來保留原始代碼的行為,因為throwables.propagate不能直接傳播受檢異常。

總之,throwables.propagate的這種用法也就馬馬虎虎,在java7中就沒必要這樣做了。在其他java版本中,它可以減少少量的代碼重複,但簡單地提取方法進行重構也能做到這一點。此外,使用propagate會意外地包裝受檢異常。

有少數api,尤其是java反射api和(以此為基礎的)junit,把方法聲明成抛出throwable。和這樣的api互動太痛苦了,因為即使是最通用的api通常也隻是聲明抛出exception。當确定代碼會抛出throwable,而不是exception或error時,調用者可能會用throwables.propagate轉化throwable。這裡有個用callable執行junit測試的範例:

<code>public</code> <code>void call() </code><code>throws</code> <code>exception {</code>

<code>        </code><code>footest.</code><code>super</code><code>.runtest();</code>

<code>    </code><code>} </code><code>catch</code> <code>(throwable t) {</code>

<code>        </code><code>throwables.propagateifpossible(t, exception.</code><code>class</code><code>);</code>

<code>        </code><code>throwables.propagate(t);</code>

<code>    </code><code>return</code> <code>null</code><code>;</code>

在這兒沒必要調用propagate()方法,因為propagateifpossible傳播了throwable之外的所有異常類型,第二行的propagate就變得完全等價于throw new runtimeexception(t)。(題外話:這個例子也提醒我們,propagateifpossible可能也會引起混亂,因為它不但會傳播參數中給定的異常類型,還抛出error和runtimeexception)

這種模式(或類似于throw new runtimeexception(t)的模式)在google代碼庫中出現了超過30次。(搜尋’propagateifpossible[^;]* exception.class[)];’)絕大多數情況下都明确用了”throw new runtimeexception(t)”。我們也曾想過有個”throwwrappingweirdthrowable”方法處理throwable到exception的轉化。但考慮到我們用兩行代碼實作了這個模式,除非我們也丢棄propagateifpossible方法,不然定義這個throwwrappingweirdthrowable方法也并沒有太大必要。

原則上,非受檢異常代表bug,而受檢異常表示不可控的問題。但在實際運用中,即使jdk也有所誤用——如object.clone()、integer. parseint(string)、uri(string)——或者至少對某些方法來說,沒有讓每個人都信服的答案,如uri.create(string)的異常聲明。

是以,調用者有時不得不把受檢異常和非受檢異常做互相轉化:

<code>    </code><code>return</code> <code>integer.parseint(userinput);</code>

<code>} </code><code>catch</code> <code>(numberformatexception e) {</code>

<code>    </code><code>throw</code> <code>new</code> <code>invalidinputexception(e);</code>

<code>    </code><code>return</code> <code>publicinterfacemethod.invoke();</code>

<code>} </code><code>catch</code> <code>(illegalaccessexception e) {</code>

<code>    </code><code>throw</code> <code>new</code> <code>assertionerror(e);</code>

我們有時也會看到propagate被用于傳播可能為受檢的異常,結果是代碼相比以前會稍微簡短點,但也稍微有點不清晰:

<code>} </code><code>catch</code> <code>(runtimeexception e) {</code>

<code>}</code><code>catch</code> <code>(exception e) {</code>

<code>    </code><code>throw</code> <code>new</code> <code>runtimeexception(e);</code>

<code>} </code><code>catch</code> <code>(exception e) {</code>

<code> </code><code>throw</code> <code>throwables.propagate(e);</code>

然而,我們似乎故意忽略了把檢查型異常轉化為非檢查型異常的合理性。在某些場景中,這無疑是正确的做法,但更多時候它被用于避免處理受檢異常。這讓我們的話題變成了争論受檢異常是不是壞主意了,我不想對此多做叙述。但可以這樣說,throwables.propagate不是為了鼓勵開發者忽略ioexception這樣的異常。

但是,如果你要實作不允許抛出異常的方法呢?有時候你需要把異常包裝在非受檢異常内。這種做法挺好,但我們再次強調,沒必要用propagate方法做這種簡單的包裝。實際上,手動包裝可能更好:如果你手動包裝了所有異常(而不僅僅是受檢異常),那你就可以在另一端解包所有異常,并處理極少數特殊場景。此外,你可能還想把異常包裝成特定的類型,而不是像propagate這樣統一包裝成runtimeexception。

<code>    </code><code>return</code> <code>future.get();</code>

<code>} </code><code>catch</code> <code>(executionexception e) {</code>

<code>    </code><code>throw</code> <code>throwables.propagate(e.getcause());</code>

對這樣的代碼要考慮很多方面:

executionexception的cause可能是受檢異常,見上文”争議一:把檢查型異常轉化為非檢查型異常”。但如果我們确定future對應的任務不會抛出受檢異常呢?(可能future表示runnable任務的結果——譯者注:如executorservice中的submit(runnable task, t

executionexception的cause可能直接是throwable類型,而不是exception或error。(實際上這不大可能,但你想直接重新抛出cause的話,編譯器會強迫你考慮這種可能性)見上文”用法二:把抛出throwable改為抛出exception”。

executionexception的cause可能是非受檢異常。如果是這樣的話,cause會直接被throwables.propagate抛出。不幸的是,cause的堆棧資訊反映的是異常最初産生的線程,而不是傳播異常的線程。通常來說,最好在異常鍊中同時包含這兩個線程的堆棧資訊,就像executionexception所做的那樣。(這個問題并不單單和propagate方法相關;所有在其他線程中重新抛出異常的代碼都需要考慮這點)

guava提供了如下三個有用的方法,讓研究異常的原因鍊變得稍微簡便了,這三個方法的簽名是不言自明的:

<a href="http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/throwables.html#getrootcause(java.lang.throwable)">throwable   getrootcause(throwable)</a>

<a href="http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/throwables.html#getcausalchain(java.lang.throwable)">list&lt;throwable&gt;   getcausalchain(throwable)</a>

<a href="http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/throwables.html#getstacktraceasstring(java.lang.throwable)">string   getstacktraceasstring(throwable)</a>