M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

リフレクションによるクラスのメンバーへのアクセスについて

佳境を(ちょっと残念なかたちで)迎えている現在の職場ですが、SpringやStrutsといういずれにせよリフレクションAPIの利用を基礎にしたフレームワークを使用するなかで、知ることになった事実についてメモ。

Javaのリフレクションの一番わかりやすい形態は、任意のクラスのメンバー(メソッド、フィールド、そしてコンストラクタ)への実行時解決型アクセスでしょう。

このうちメソッドとフィールドについては、任意のクラスについて名前(とメソッドの場合は引数型も)を指定してメソッドへアクセスする方法と、そのクラスのメソッドもしくはフィールドすべてを格納した配列を取得してアクセスする方法とがあります。

重要なことはさらにこれらの2種類のAPIには、

  1. そのクラスが継承・実装により備えているpublicなメソッドを返すものと、
  2. そのクラスがまさに自分自身で宣言しているメソッドをアクセスレベルに関係なく返すもの

──という2種類があることです。
コンストラクタは継承はされませんのでこうした分類はありえません。)

返り値 シグネチャ
Field getField(String name)
Field[] getFields()
Field getDeclaredField(String name)
Field[] getDeclaredFields()
Method getMethod(String name, Class... parameterTypes)
Method[] getMethods()
Method getDeclaredMethod(String name, Class... parameterTypes)
Method[] getDeclaredMethods()

例えば、KlassAとKlassBという親子クラスがあったとします。

public class KlassA {
	public String foo(){
		return "foo";
	}
	private String bar(){
		return "bar";
	}
}
public class KlassB extends KlassA {
	public String foo2(){
		return "foo2";
	}
	private String bar2(){
		return "bar2";
	}
}

このときKlassBのメソッドにリフレクションでアクセスするとどうなるでしょうか。

public class ReflectionStudy {
	public static void main(String[] args) {
		// KlassAクラスに対応するClassクラス・インスタンスを取得
		Class<KlassB> b = KlassB.class;
		
		// 2つのタイプのリフレクションAPIで、それぞれKlassBのメソッドを取得
		Method[] bMethods = b.getMethods();
		Method[] bDeclairedMethods = b.getDeclaredMethods();
		
		StringBuilder sb = new StringBuilder();
		
		// getMethods()メソッドの返り値をチェック
		String s = "";
		sb.append("b.getMethods() -> [");
		for(Method m: bMethods){
			s += (s.length() == 0 ? "" : ", ") + m.getName();
		}
		sb.append(s).append("]\n");
		
		// getDeclairedMethods()メソッドの返り値をチェック
		s = "";
		sb.append("b.getDeclaredMethods() -> [");
		for(Method m: bDeclairedMethods){
			s += (s.length() == 0 ? "" : ", ") + m.getName();
		}
		sb.append(s).append("]");

		System.out.println(sb);
	}
}

System.out.println(sb);の出力は次のようになります。

b.getMethods() -> [foo2, foo, wait, wait, wait, equals, toString, hashCode, getClass, notify, notifyAll]
b.getDeclaredMethods() -> [foo2, bar2]

2種類のAPIを使用してKlassBクラスのメソッドを走査すると、一方ではObjectにまでさかのぼるpublicメンバーがすべて取得され、もう一方ではKlassBで宣言されたメンバーがアクセスレベルに関係なく取得されます。

2つのAPIそれぞれの制約

2つのAPIにそれぞれこのような一種の制約のあることは、論理的に考えて納得できないわけでもありません。

Objectまでさかのぼる継承ツリーの中には、子孫クラスによるオーバーライドや、privateやデフォルトアクセスであるために隠蔽されていた同名メンバーが存在する可能性があります。“Declared”なしのAPIはこの問題に対してpublicなメンバーへのアクセスのみ提供することで対処しています。

この種の問題への解決は明らかに1種類ではありませんから、標準APIは慎ましやかにしているということなのかもしれません。

一方で“まさにそのクラスで宣言されたメンバー”という条件がつけば、アクセスレベルに関わりなく、一意性がおおよそ確保され得ますから(Java言語仕様的にはそうなのですが、JVMの仕様的にはそうでもないらしいです)、“Declaired”ありのAPIではそのクラスで宣言されたすべてのメンバーへのアクセスを提供します。

継承されたメンバーへのアクセス

今回の職場で、メンバーアクセスのためのリフレクションAPIの特性に関連して問題になったのは──あとから考えると当然至極のことなのですが──、任意のクラス・インスタンスのprivateなメンバーにリフレクションでアクセスし、その値を検証・加工したのち再代入する、という処理です。

もうすこし具体的にいうと、これはStruts2のValidationの機能を利用して、カスタムバリデーションを作成した際に問題になったことです。ユーザーの入力の不正を検証して一部伏せ字などをほどこして入力欄に再表示するというものです。

Struts2ではユーザーの入力およびユーザーへの出力は、ActionクラスのフィールドにGetter/Setterを使用して取得・設定されます。このActionのフィールドにリフレクションでアクセスするわけです。

検証対象のフィールド名はあらかじめコンフィギュレーションファイルで決定しているため、検証ロジック設計製造者としては、①そのフィールドへ名前でアクセスし、②その値をリフレクションで取得し(実際にはこの取得処理はStruts2のValidationプラグインが行います)、③その値を処理して、④再度リフレクションでフィールドそのものにアクセスして処理済みの値を代入すればよい、と考えたわけです。

このようなロジックの問題はリフレクションでアクセスする対象のクラスが、別の何らかのクラスを継承しているとき、その継承先クラスで宣言されたフィールドにはアクセスできないことです。

Strutsの、しかもValidationという時点で、処理対象はBeanクラスであることはあきらかなのですから(そして対象のフィールドの名称も知れているのです)、このような場面におけるよりよいロジックとは、“Declaired”なしのAPIでフィールドアクセスを試みることです。

ここでは単純にフィールド名の先頭を大文字にして、その前側に“set”を連結した名前でメソッドへアクセスするのです。

String setterName = "set" + fieldName.substring(0,1).toUpperCase() + fieldName.substring(1);
Method setter = KlassC.class.getMethod(setterName, String.class);
setter.invoke(targetInstance, processedValue);

実際には単にフィールドやメソッドを代理するクラスインスタンスを取得したときとはちがって、それらの値にアクセスしたり、処理を実行したりする場合には、多くのチェック例外への対応が必要ですが、上の例では省きました(invokeメソッドの使用だけで5つの例外に対処する必要があります)。