例外の取扱いに関するベストプラクティス

前回に続いて今度はクライアントコード側の作法について書きます。
前回と同様、下記のサイトを引用しながら書いていきたいと思います。

http://onjava.com/onjava/2003/11/19/exceptions.html

前回:例外に関するAPIデザインのベストプラクティス

前回と同様に、斜体部分は引用です。

1. Always clean up after yourself
(自分の始末は自分でつけましょう)

リソースは確実にtry-finallyを使って開放すべきということが述べられています。
これについては、示されているサンプルよりも、もう少し掘り下げて検討するべきと思うため、私の考えを記述します。


public void executeQuery() {
    Connection con = null;
    try {
        con = getConnection();

        // do something
  
    } catch (SQLException e) {
        throw new RuntimeException(e);
    } finally {
        try {
            con.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

この例にはいくつかの問題があります。

まず一つ目、getConnection()で例外が発生した場合に、conはnullとなり、con.close()を呼び出したところで、NullPointerExceptionが発生してしまいます。
getConnection()に失敗したときにはcatchブロックに入りますが、このとき、発生した例外は、このNullPointerExceptionによって上書かれてしまい、このメソッドのクライアントコードには、NullPointerExceptionがエスカレーションされることになります。このとき、本来の例外であるgetConnection()の呼び出し時に発生した例外は失われてしまいます。

二つ目に、do somethingの部分でSQLExceptionが発生した場合を考えます。この場合、まずはcatchブロックに入り、RuntimeExceptionがエスカレーションされます。しかし、この場合、最後に呼ばれるcon.close()は、例外を投げる可能性があります。その場合、元々発生した例外は、この時発生した二つ目のRuntimeExceptionによって上書きされてしまい、情報が失われてしまいます。

少し面倒ですが、リソースのクローズは以下のように書くべきと私は考えています。



public void executeQuery() {
    Connection con = null;
    try {
        con = getConnection();
  
        // do something
  
    } catch (SQLException e) {
        throw new RuntimeException(e);
    } finally {
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}


ポイントとしては、以下の二点しょうか。

  • クローズの前にnullチェックをいれる
  • finallyブロックの中で発生した例外への対応はログ出力に留めエスカレーションしない
特に、強調したいのは、finallyブロックの中で例外を発生させないと言う点です。
ここで発生した例外は、意図せず、tryブロック内や、catchブロックからエスカレーションされた例外を上書きしてしまうことがあります。

リソースはfinallyの中で開放するというプラクティスと同時に、上記の二点を加えて書いておきたいと思います。

Java7になればtry-with-resource構文が使えてこういうプラクティスも古いものになってしまうわけですが…

2. Never use exceptions for flow control
(フローの制御に例外を使わないようにしよう)

例外を発生させることでフローを制御してはいけないということがかかれています。
例外オブジェクトはスタックトレースを含みます。この情報はデバッグのためには大変有用なものですが、生成には高いコストを払う必要があります。

引用元のサイトでは、以下のようなサンプルを悪い例として紹介しています。


public void useExceptionsForFlowControl() {
    try {
        while (true) {
            increaseCount();
        }
    } catch (MaximumCountReachedException ex) {
    }
    //Continue execution
}

public void increaseCount()
    throws MaximumCountReachedException {
    if (count >= 5000)
        throw new MaximumCountReachedException();
}


コードとしては成立していませんが…言いたいことはつまり、例外をつかってループを抜けるというようなことをしてはいけないといいうことです。

3. Do not suppress or ignore exceptions
(例外を隠したり無視してはいけません)

APIから検査例外がスローされてくる場合は、APIは何かしらの回復処理を要求していると考えられます。もし、特に何もできない場合でも、非検査例外としてスローしなおすべきで、無視するべきではありません。

4. Do not catch top-level exceptions
(トップレベル例外をキャッチしてはいけません)

この記事では、下記のようにExceptionをキャッチしてしまうと、RuntimeExceptionまでキャッチされ、無視されてしまうという問題を指摘しています。

try{
    //..
}catch(Exception ex){
}

例えば、あるメソッドがコールされた際に、接続異常のためIOExceptionが発生したときに行う回復処理と、プログラミングエラーのためにNullPointerExceptionが発生した際に行う処理は異なるはずです。
前者の場合は、リトライなどの復旧処理の後、成功すれば処理を継続という可能性がありえますが、後者の場合は、異常を記録してスレッドの処理を中断すべきです。

このように、発生する例外のタイプによって、記述される処理は異なるはずです。
例外はできるだけ具象クラスを用いてキャッチすべきです。

5. Log exceptions just once
(例外の記録は一度だけにしましょう)

これは当たり前ですが、例外が一回しか発生していないにも関わらず、同じスタックトレースが何度もログに出ていたら、ログの読み手は混乱します。

これを実現するためには、例外はできるだけ上位レイヤーでまとめて補足することが大切です。

例えば私は、mainメソッドや、フレームワークとの境界レイヤーでは以下のように必ずThrowableをキャッチするコードを書くことにしており、特に、非検査例外についてはここでまとめてログを出力することにしています。


try {
    new Main().execute();
} catch(Throwable e) {
    e.printStackTrace();
}

その他では、例外をキャッチした後、正常処理に復帰する場合にだけ、ログにその例外を記録するというルールでコードを書いています。


public void perform() {
    try {
        query();
    } catch(SQLException ex ) {
        ex.printStackTrace();  // ログへ出力
    }
    //continue to perform
}

実際には、printStackTraceの代わりに、LoggerなどのロギングAPIを用いて、warningや、debugなどのログレベルでログに出力します。処理が継続できるということは、致命的なエラーというわけではないはずですので、出力されたログファイル内で、致命的なものと区別できると便利だからです。




前回から続いて、例外の取扱いに関するベストプラクティスをまとめて見ました。
例外の取扱いに関する資料は少ないですね。
だいたいみんな例外処理とか好きじゃないだろうしね…

ツッコミなどあれば歓迎です。

コメント

Unknown さんの投稿…
例外についてまとまってる文章で, 読んでて面白かったです.

ログに関して,「例外が起きた場所でしか参照できない変数の値を, 記録もしくは伝達する」という視点が無いな, と読んでて思いました.

私自身は以前は, 例外が起きたその場で関係する値の記録のためにログを吐いて, 必要であれば例外を投げる, という考え方でした.
でもこれをやってしまうと, ライブラリとして再利用しづらい (余計なログが出る) ので, 例外オブジェクトに値を記録して投げるのが良い, という考えに変わっています. これのために独自例外クラスは増えますが, 値の記録をする目的があれば作る理由になるでしょう.

ここらへんも議論できたら嬉しいです.
G.O. さんの投稿…
コメントありがとうございます。
確かに、この視点は抜けてますね。

例外が起きた場所でしか参照できない変数をどうするかということに関して少し考えてみたのですが、これも2パターンに分けられて、

・クライアントでその値を用いて有効な復旧ができる場合
・クライアントも記録するぐらいしかやることがない場合

前者の場合は、チェック例外を自作して、例外オブジェクトにgetHoge()などというメソッドを付け足してしまうのがいいと思われます。

後者の場合は、非検査例外のメッセージとして埋め込むのがよいのではと思われます。

私の経験上ほとんどの場合後者です。

ライブラリ内でログを吐くのは、ライブラリの再利用性の観点から私もあまりよくないと思っています。
eller さんの投稿…
nullチェックですが、以下のように書けばもっとシンプルにできるのでオススメです。getConnectionが通知例外を投げない場合はこれで十分かと。

final Connection connection = getConnection();
try {
  // ...
} finally {
  connection.close();
}


参考:http://www.hyuki.com/dp/dpinfo.html#BeforeAfter
eller さんの投稿…
通知例外→検査例外ですね。失礼。