かまたま日記3

プログラミングメイン、たまに日常

SpringAOPが適用されたbeanを使ったメタプログラミングでハマった件

※注意

以下の話はSpringAOP3.1.1で確認しています。

SpringAOPで処理が挟み込まれたbeanは実体がProxyクラスなので、beanを使ってメタプログラミングをしたい場合、ちょっと困ることがありました。

困ること例

例えばCommandパターンを利用した以下のようなCommandのComponentがあったとします。

public interface Data {
	void readData(DataSource source);
}

public interface Command<T extends Data> {
	void execute(T data);
}

@Component
public class SampleCommand implements Command<SampleData> {
	@Override
	public void execute(SampleData data) {
	}
}

public class SampleData implements Data {
	@Override
	public void readData(DataSource source) {
	}
}

この時に以下のようなコマンドを処理する共通処理なんかでCommandクラスの型パラメータを利用したいとします。
(nullチェックとか配列の要素チェック的なエラーハンドリングは除外しております)

public void executeCommand (Command command, DataSource source) throws Exception{
	ParameterizedType ptype = (ParameterizedType)command.getClass().getGenericInterfaces()[0];
	Data data = (Data)((Class)ptype.getActualTypeArguments()[0]).newInstance();
	data.readData(source);

	command.execute(data);
}

で、コマンド毎の共通処理でAOPを使いたいってことで、CommandクラスにSpringAOPを適用して生成されたbeanをexecuteCommandメソッドに渡すと以下の例外が発生します。
(SpringAOPの使い方は略)

 java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType

executeCommandメソッドの最初の行のParameterizedTypeへのキャストが失敗しております。
変数の中身を見てみると、確かに型パラメータの情報が消えております。
java.lang.reflect.Proxyの仕様なのか、SpringAOPのProxyの仕様なのかは良く分かってませんが)

解決策

AOPが適用されているクラス用処理を以下のように分けます。

public void executeCommand (Command command) throws Exception{

	Class dataClass = null;
	if(Proxy.isProxyClass(command.getClass())) {
		// プロキシクラスの場合は元のクラスを取得する
		final InvocationHandler handler = Proxy.getInvocationHandler(command);
		for (Field field : handler.getClass().getDeclaredFields()) {
			if(field.getType().equals(AdvisedSupport.class)) {
				field.setAccessible(true);
				AdvisedSupport as = (AdvisedSupport)field.get(handler);
				dataClass = as.getTargetClass();
			}
		}
		if(dataClass == null) {
			throw new IllegalArgumentException();
		}
	} else {
		dataClass = command.getClass();
	}

	ParameterizedType ptype = (ParameterizedType)dataClass.getGenericInterfaces()[0];
	Data data = (Data)((Class)ptype.getActualTypeArguments()[0]).newInstance();
	data.readData(source);

	command.execute(data);
}

SpringAOPが使っているInvocationHandlerの実装は
org.springframework.aop.framework.JdkDynamicAopProxy
で、そのメンバである
org.springframework.aop.framework.AdvisedSupport
クラスをリフレクションを使って引っこ抜き、getTargetClassメソッドで実際のプロキシ元のクラスを取得しています。