技術とか戦略とか

SIerで証券レガシーシステムを8年いじってからSESに転職した普通の文系SEによる技術ブログ。

java:例外発生後に例外のメッセージを書き変える

例外オブジェクトにはメッセージが格納されており、getMessage()メソッドでそのメッセージを取得することができます。
しかし、このメッセージはコンストラクタでのみ設定可能であり、メッセージを後で変更するメソッドは用意されていないため、例外クラスに用意されている手段では例外発生後にメッセージを書き変えることはできません。
 
しかし、リフレクションを使用することで、メッセージを後で書き変えることが可能です。
メッセージはThrowableクラスのprivateのクラス変数"detailMessage"に保持されるため、これをリフレクションで書き変えます。
 
サンプルコードは以下の通りです。
 
【サンプルコード】
・ExceptionTest.java
import java.io.IOException;
import java.lang.reflect.Field;

public class ExceptionTest {

  public static void main(String[ ] args) {

    try {
      method();
    } catch (IOException e) {
      System.out.println(e.getMessage());
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  static void method() throws Exception {

    try {

      // メッセージ付きで例外を発生させる
      throw new IOException("hoge");

    } catch (Exception e) {

      // detailMessageフィールドの定義を取得
      Field fieldDefinition =
        Throwable.class.getDeclaredField("detailMessage");

      // フィールドをアクセス可能に設定
      fieldDefinition.setAccessible(true);

      // 例外オブジェクトのメッセージを変更する
      fieldDefinition.set(e, e.getMessage() + "fuga");

      // 例外を再スロー
      throw e;

    }

  }

}
 
【実行結果】
・コンソール(標準出力)
hogefuga
 
・コンソール(標準エラー出力
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by ExceptionTest (file:/C:/pleiades/workspace/Hello/build/classes/) to field java.lang.Throwable.detailMessage
WARNING: Please consider reporting this to the maintainers of ExceptionTest
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
 
----
 
ちなみに、リフレクション時に発生してしまう標準エラー出力ですが、筆者の環境(Java10)では抑止する良い手段が見つかりませんでした。
以下の2つの手段を試しています。
 
1.JVM引数で抑止
"--illegal-access=deny"を指定したら逆に異常終了するようになってしまいました。
下記ページによると、Java11も不可、Java8は可(そもそもリフレクション時の標準エラー出力が出ない)だそうです。
 
Java - Javaの開発環境を作る過程でのエラー文について|teratail
https://teratail.com/questions/153595
 
2.標準エラー出力の出力ストリームを変更
通常、"System.setErr(自作PrintStreamオブジェクト);"を記述することで標準エラー出力の出力先を変更し、コンソールに出力されないようにすることができるのですが、今回のケースではそれができませんでした。
"System.err.close();"を記述した場合はコンソールへの出力を抑止することができたので、リフレクション時のWARNINGメッセージでは"System.err"を直接使用しているのではないかと思います。
なお、"System.err"はクローズしたらリオープンすることができないので、"System.err.close();"を実行してしまうと、以降は予期せぬ例外等が発生しても標準エラー出力として出力できなくなってしまいます。
("System.setErr(System.out);"で標準出力として出力することならできますが…)