【Java】リフレクション -メソッド実行時の例外の取得-

はじめに

リフレクション利用時の、メソッド実行時の例外の取得方法について記述する。
リフレクションを使った、特定のメソッド実行の大まかな手順は以下のとおりである。
1. 実行元となるクラスを作成する。
2. Classオブジェクトを生成する。(getClassメソッド
3. 生成したClassオブジェクトを使ってMethodオブジェクトを生成する。(getDeclaredMethodメソッド
4. 実行させようとしているメソッドがpublicでなければ、外からメソッドを実行できるようにする。(setAccessibleメソッド
5. (必要に応じて引数を渡して)メソッドを実行する。(invokeメソッド

手順5にて、リフレクションを使ってメソッドの実行を行うわけだが、実行元のクラスのメソッドを実行したときに発生した例外は「InvocationTargetException」にラップされてしまい、そのままでは実際に発生した例外を知ることができない。

ラップされた例外を取得するためには、InvocationTargetExceptionのgetTargetExceptionメソッドを用いる。

クラス設計

サンプルクラスXを作成した。概要は以下の通り。
【フィールド】
・String型のフィールドを持つ。

コンストラクタ
・Stringのパラメタを受け取り、その値をフィールドにセットする。
 (受け取ったパラメタがnullならNullPointerExceptionをスローする)
・引数を受け取らないコンストラクタを用意する。

メソッド
◎appendString
・Stringのパラメタを受け取り、フィールドの文字列と連結して返すpublicメソッド
・パラメタがnullならNullPointerExceptionをスローする。
・文字列の連結処理自体はexecuteAppendStringで行う。メソッド呼出しのパラメタは、本メソッドのパラメタを用いる。
・executeAppendStringメソッドは本メソッドから呼ばれる。

◎executeAppendString
・appendStringメソッドから呼ばれるprivateメソッドであり、文字列の連結の実質的な処理は本メソッドで行う。
・フィールドにセットされている文字列とパラメタを連結して返す。

package myReflection;

public class X {
	private String str;

	/**
	 * Set field value if parameter is not null.
	 *
	 * @param str
	 * @throws NullPointerException
	 *             if parameter is null.
	 */
	public X(String str) {
		final String CONSTRUCTOR_INFO = "class X constructor with String parameter";
		if (null == str) {
			throw new NullPointerException("Parameter is null. @" + CONSTRUCTOR_INFO);
		}
		this.str = str;
	}

	/**
	 * If created X object without parameter, set default value.
	 */
	public X() {
		this("Hello, ");
	}

	/**
	 * Append field "str" and String parameter "param".
	 * This method calls executeAppendString.
	 *
	 * @param param
	 * @return appended String object.
	 * @throws NullPointerException
	 *             if parameter is null.
	 */
	public String appendString(String param) {
		final String METHOD_NAME = "X #appendString with String parameter";
		if (null == param) {
			throw new NullPointerException("Parameter is null. @" + METHOD_NAME);
		}

		return executeAppendString(param);
	}

	/**
	 * Append field "str" and String parameter "param".
	 *
	 * @param param
	 * @return appended String object.
	 */
	private String executeAppendString(String param) {
		final String METHOD_NAME = "X #executeAppendString with String parameter";

		return str += param;
	}
}

メソッドの実行 (正常系)

作成したXクラスを、リフレクションで実行すると、コンソール上に「Hello, world!」と表示される。
(x.appendString("world!") の結果を標準出力すれば1行で終わるじゃん、というツッコミは無しで)

package myReflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

	public static void main(String[] args) {
		// Create object.
		X x = new X();

		// Get Class object.
		Class clazz = x.getClass();

		// Get Method object with Class object.
		Method method = null;
		try {
			method = clazz.getDeclaredMethod("appendString",
					String.class);
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		}

		// Set accessible true if method is not public method.
		if (!method.isAccessible()) {
			method.setAccessible(true);
		}

		// Invoke method with reflection.
		try {
			String result = (String) method.invoke(x, "world!");
			System.out.println(result);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
	}
}

メソッドの実行 (異常系)

正常系のときは先述のとおりで仕様どおりの動作をしてくれているので問題ない。
では、異常系のときはどうか。XクラスのappendStringメソッドは、パラメタがnullならNullPointerExceptionをスローすることになっている。
ので、nullを渡してinvokeしてみる。

// Invoke method with reflection.
try {
	String result = (String) method.invoke(x, (Object)null);
	System.out.println(result);
}

仕様では、パラメタがnullならNullPointerExceptionがスローされるので、「NullPointerExceptionが発生したぞ!」と出てきてほしいが、実際コンソール上に表示されるのは以下の通り。

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at myReflection.Main.main(Main.java:33)
Caused by: java.lang.NullPointerException: Parameter is null. @X #appendString with String parameter
	at myReflection.X.appendString(X.java:40)
	... 5 more

メソッドをinvokeしたとき、「InvocationTargetExceptionが発生したぞ!」と表示される。
しかし、よく見ると「NullPointerExceptionが原因で、最終的にInvocationTargetExceptionがスローされている」ことがわかる。
本来発生した例外は「NullPointerException」なのに、実際投げられる例外は「InvocationTargetException」なのである。
つまり、実際に発生した「NullPointerException」は、「InvocationTargetException」にラップされていることを意味しており、実際発生した例外を取得するには、ワンクッションおく必要がある。

どうすればいい?

結論から言うと、「InvocationTargetException」クラスに定義されている「getTargetException」メソッドを用いるとよい。
このメソッドの定義は、JavaDocには以下のように書かれている。

getTargetException

public Throwable getTargetException()
スローされたターゲット例外を取得します。  
このメソッドは汎用的な例外チェーン機能に先行します。この情報を取得するために、Throwable.getCause() メソッドを使用することをお勧めします。

戻り値:
スローされたターゲット例外 (この例外の原因)

つまり、getTargetExceptionメソッドを呼び出すことによって、メソッド実行におけるおおもとの例外の情報を取得することができる。
InvocationTargetExceptionのcatch節を、以下のように書き換えると・・・

// Invoke method with reflection.
try {
	String result = (String) method.invoke(x, (Object)null);
	System.out.println(result);
…
} catch (InvocationTargetException e) {
	System.out.println(e.getTargetException());
}

以下のようにコンソール上に表示される。

java.lang.NullPointerException: Parameter is null. @X #appendString with String parameter

ちゃんとNullPointerExceptionが表示されている。

補足だが、getTargetExceptionメソッドの実行結果に対してgetClassメソッドを使うと、発生した例外の実行時クラスの情報を取得できる。
つまり、以下のように記述すると・・・

System.out.println(e.getTargetException().getClass());

以下のように表示される。

class java.lang.NullPointerException

これができることにより、リフレクションでメソッドを実行したときに返ってきた例外に応じて処理を分岐させるといったことも可能になる。
リフレクションでそこまでやるか?という疑問は置いといて・・・。

ちょっと考察

呼出しもとでNullPointerExceptionのcatch節を用意すればいいんでないの?
つまり、以下のように書けば、コンソール上には「ぬるぽ」と表示されるのではないか。

// Invoke method with reflection.
try {
	String result = (String) method.invoke(x, (Object)null);
	System.out.println(result);
} catch (NullPointerException e) {
	System.out.println("ぬるぽ");
…
} catch (InvocationTargetException e) {
	System.out.println(e.getTargetException());
}

答えは「ノー」である。
今回のプログラムを例にとると、先述の通り、メソッドのinvokeでは「(NullPointerExceptionがラップされた)InvocationTargetExceptionがスローされる」ため、呼出しもとでNullPointerExceptionのcatch節を用意したとしても、その中に処理が入ることはない。
InvocationTargetExceptionをcatchし、catchした例外に対してgetTargetExceptionメソッドを呼んで、おおもとの例外を取得する必要がある。

一応、上記の場合の結果を掲載しておく。「ぬるぽ」と表示されていないことがわかる。

java.lang.NullPointerException: Parameter is null. @X #appendString with String parameter

おわりに

リフレクション利用時の、メソッド実行時の例外の取得方法について、サンプルコードを交えて記述した。
リフレクションで、あるクラスのメソッドを実行したときに発生する例外は、InvocationTargetExceptionにラップされている。
ラップされている例外は、InvocationTargetException #getTargetException を使うことで得ることができる。
ちなみに、今回はpublicメソッドをリフレクションで実行したが、Methodオブジェクトに対してsetAccessibleメソッドを実行し、trueを渡すことで、privateメソッドも外から呼べるようになる。テストコードを書くときなどに使える。

参照

InvocationTargetException (Java Platform SE 6)