0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaのtry-with-resources:本当の原因を隠さないsuppressed例外の読み方

0
Posted at

ファイルやDB接続を使うJavaの処理では、try-with-resourcesを使うと終了時にclose()を呼び出せます。

ただし、本来の処理も、後片付けのclose()も失敗した場合、どちらの例外が残るのでしょうか?

障害調査で「接続を閉じるときのエラーだけを見て、SQL実行時の失敗を見落とす」といった混乱を防ぐため、Javaのsuppressed exception(抑制された例外)を実行例で確認します。

対象:Java 17以降で動くサンプル。言語仕様は2026年10月26日時点のJava SE 27の公式仕様で確認し、サンプルはOpenJDK 21で--release 17を指定してコンパイル・実行しました。

1. try-with-resourcesは何をしている?

リソースとは、使用後に解放が必要なファイル、ストリーム、DB接続などのことです。

AutoCloseableというインターフェース(クラスが実装する機能の契約)のclose()を持つオブジェクトをtry (...)に記述すると、処理が正常終了しても例外終了しても、後片付けのためにclose()が呼ばれます。

try (var input = java.nio.file.Files.newInputStream(path)) {
    // ファイルを読み込む
} // ここを抜けるとinput.close()が呼ばれる

pathはjava.nio.file.Path型の変数を想定した抜粋です。

ただし、「close()を試みる」ことと「解放が必ず成功する」ことは別です。close()自身が例外を投げる場合もあります。

2. 本体とclose()が両方失敗するとどうなる?

そのまま動かせる例をSuppressedExceptionDemo.javaとして保存します。

import java.io.IOException;

public class SuppressedExceptionDemo {
    static final class Resource implements AutoCloseable {
        private final String name;

        Resource(String name) {
            this.name = name;
        }

        @Override
        public void close() throws IOException {
            System.out.println("close: " + name);
            throw new IOException("close " + name + " failed");
        }
    }

    public static void main(String[] args) {
        try (Resource first = new Resource("first");
             Resource second = new Resource("second")) {
            throw new IllegalStateException("operation failed");
        } catch (Exception e) {
            System.out.println("primary: " + e.getMessage());
            for (Throwable suppressed : e.getSuppressed()) {
                System.out.println("suppressed: " + suppressed.getMessage());
            }
        }
    }
}

ターミナルで次の2コマンドを実行します。

javac --release 17 SuppressedExceptionDemo.java
java SuppressedExceptionDemo
  • javac:Javaのソースをコンパイル(実行できるクラスファイルへ変換)するコマンド。
  • --release 17:Java 17向けの言語・API・クラスファイルとしてコンパイルするオプション。JDK 17以上のjavacが必要です。
  • java SuppressedExceptionDemo:生成したクラスのmainメソッドを実行するコマンド。

実行結果:

close: second
close: first
primary: operation failed
suppressed: close second failed
suppressed: close first failed

ここで確認できるのは、次の3点です。

  1. 複数のリソースは、宣言と逆順にclose()される(second → first)。
  2. tryの本体で発生したIllegalStateExceptionが主例外(外側へ通知される中心の例外)になる。
  3. 後片付け時の2つのIOExceptionは消えず、主例外のgetSuppressed()から取得できる。

**抑制された例外(suppressed exception)**は「無視されるエラー」ではありません。主例外を上書きせずに、後から診断できるよう保存された例外です。

3. 本体が成功した場合は?

本体のthrow new IllegalStateException(...)を、例えばSystem.out.println("operation ok");に置き換えてみてください。

この場合、先に失敗するsecond.close()のIOExceptionが主例外になります。後から失敗するfirst.close()のIOExceptionは、その主例外のgetSuppressed()に入ります。

つまり、常に本体側の例外が主例外になるのではなく、先に発生した失敗の有無で決まる、ということです。

Java言語仕様では、複数リソースのtry-with-resourcesは入れ子の処理に相当し、後から宣言したリソースから閉じると定義されています。

4. getCause()とgetSuppressed()を混同しない

Javaの例外には、似ている2つの参照があります。

API 意味 代表例
getCause() この例外の原因となった別の例外 new IllegalStateException("保存失敗", sqlException)
getSuppressed() 主例外を優先したため、外側へ直接投げなかった別の例外 try本体の失敗に続くclose()の失敗

例えば業務例外に変換するときは、元の例外を原因(cause)として保持します。

try {
    saveOrder();
} catch (java.sql.SQLException e) {
    throw new IllegalStateException("注文の保存に失敗しました", e);
}

saveOrder()はSQLExceptionを投げ得る処理を表す抜粋です。こうしておけば、元の例外のスタックトレースやsuppressed情報をgetCause()経由でたどれます。

反対に、new IllegalStateException(e.getMessage())だけで包むと、原因の例外オブジェクトを失います。

5. JDBCでの使いどころ

JDBCは、JavaからリレーショナルDBにアクセスするための標準APIです。Connection、PreparedStatement、ResultSetなどのリソースを扱います。

以下は、DataSource dataSource(接続を取得する仕組み)とlong userIdが利用可能なメソッド内の抜粋です。

String sql = "SELECT name FROM users WHERE id = ?";

try (java.sql.Connection connection = dataSource.getConnection();
     java.sql.PreparedStatement statement = connection.prepareStatement(sql)) {
    statement.setLong(1, userId);

    try (java.sql.ResultSet rows = statement.executeQuery()) {
        if (rows.next()) {
            System.out.println(rows.getString("name"));
        }
    }
} catch (java.sql.SQLException e) {
    // ロガーに例外オブジェクトを渡すと、原因やsuppressedの追跡に役立つ
    e.printStackTrace(); // 説明用。本番では利用中のロガーを使う
    throw new IllegalStateException("ユーザー取得に失敗しました", e);
}

このコードは自前で接続を取得・所有するケースの例です。Springの@TransactionalやJdbcTemplateなど、フレームワークが接続・トランザクションを管理する場合には、そちらの管理方法に従ってください。むやみに接続を手動で閉じる実装へ置き換えないことが重要です。

6. 障害調査とテストでのハマりどころ

  • close()側のログだけで原因を断定しない:主例外の種類、getCause()、getSuppressed()を確認します。printStackTrace()は通常、抑制された例外も表示します。
  • 例外メッセージだけを再throwしない:元のThrowableをcauseに渡して保持します。
  • close()が必ず成功すると思わない:自動で呼ばれても、I/O障害などでクローズは失敗し得ます。
  • 手書きのfinallyへ安易に戻さない:finally中の例外で本体の例外が置き換わる書き方をすると、調査すべき失敗を見失います。
  • テストでは2種類の失敗を分ける:「処理中に失敗+closeも失敗」と「処理は成功+closeで失敗」の両方を試すと、主例外の違いを検証できます。

まとめ

try-with-resourcesの利点は後片付けを短く書けることだけではありません。本体処理の失敗を主例外として残し、後片付けの失敗もsuppressedとして保持することで、障害の全体像を追いやすくなります。

本番ログでSuppressed:という行を見つけたら、無関係な情報として切り捨てず、主例外とセットで読むのが重要です。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?