Seasar2でアノテーションを使って自動AOP

今までの日記の流れをぶった切って、突然ですがSeasar2のお話。
今度仕事で使うことになりまして、今更ながら勉強を始めました。DIコンテナには前々から興味があって、Springを一通り触ったりはしていたのですが、仕事で使う際に、やはり少しでも設定ファイルを少なくして簡単に作れる方がいいと思い、S2を採用しました。
Seasar2はSpringと比べて、DIコンテナに登録するコンポーネントの記述を少しでも少なく出来るような工夫が多く、その点で開発者にとって優しい仕様になってるのではないかと思います。インターフェイスを実装したコンポーネントをコンテナ側が自動バインデングする機能は、開発者が訳分からなくなって誤動作を引き起こす事態になりはしないかとか心配していたのですが・・・業務内容によって細かく設定ファイルを分割すれば、ほとんどそんな心配は無くなるのですね。インターフェイスを介しての疎結合さえ心掛けていれば、コンポーネントの組み立てについては、殆ど設定ファイルを書く必要が無い所がお気に入りです。
今回はサーバサイドの開発なので、インスタンス管理については少し注意が必要でしょうか。S2はデフォルトではシングルトンのコンポーネントを作成するので、オブジェクトがスレッドセーフかどうかをチェックして設定ファイルに登録する必要があります。ここら辺のケアレスミスがちょっと心配かも?
で、なんだかんだで作り始めて・・・ちょっと気になったのがAOP機能でした。S2の設定ファイルは、コンポーネントの設定の中にアスペクト関連の記述を書く必要があり、基本的に「登録したコンポーネント全てに関して、一定の法則でアスペクトを適用する」みたいな設定をすることが出来ません。この為、アスペクト関連の記述をコンポーネント毎に繰り返し繰り返し行うことになり、結果として設定ファイルの記述量が増大します。また、ポイントカットで動作するメソッドを選択することは出来ますが、同名のメソッドが複数ある「オーバーロード」状態のメソッドの中からどれか一つを選択することが出来ません。何か方法が無いかと思い、2ch等で質問したりして調べていたのですが・・・現在EA1が出ているSeasar4のAutoRegisterという新機能が参考になるとのヒントを頂き、いろいろ調べてみました。
最終的に、コンテナに登録しているコンポーネント設定を一個ずつ取り出して、そこに新たにアスペクト設定を追加できることがわかりました。インターフェイスの特定メソッドに一律的にアスペクトを設定できるので、AOPの記述が一気に激減し、かなり設定ファイルの量が減りました。
・・・と、ここまでは別に日記に書くような内容では無かったのですが、コンポーネント設定を一つずつ見れるってことは、JDK5.0ならアノテーションアスペクトの設定が出来るってことじゃないか?・・・と思い、早速作ってみました。

AutoRegisterインターフェイス

自動AOP設定を行うクラスに実装させるインターフェイス

package test;

public interface AutoRegister {
	void registAll();
}

AbstractRegisterクラス

自動AOP設定を行う基底クラス(コンテナのコンポーネントを一つずつ取り出す方法って、これでいいのかな?・・・)

package test;

import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.S2Container;

public abstract class AbstractAutoRegister implements AutoRegister {

	private S2Container container;

	public void registAll() {
		S2Container root = container.getRoot();
		registContainer(root);
		for (int i = 0; i < root.getChildSize(); i++) {
			registContainer(root.getChild(i));
		}
	}

	private void registContainer(S2Container ct) {
		for (int i = 0; i < ct.getComponentDefSize(); i++) {
			ComponentDef cd = ct.getComponentDef(i);
			regist(cd);
		}
	}

	protected abstract void regist(ComponentDef componentDef);

	public S2Container getContainer() {
		return container;
	}

	public void setContainer(S2Container container) {
		this.container = container;
	}
}

AnnotationAutoRegisterクラス

このクラスに指定したアノテーションを記述しているコンポーネントを探して、AOP定義を追加するクラス(コンポーネント定義の初期化方法がよく分からず、ソースにそれっぽいことが書かれてあったdestroyメソッドとinitメソッドを呼んでます(汗))

package test;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import org.seasar.framework.aop.Pointcut;
import org.seasar.framework.aop.impl.PointcutImpl;
import org.seasar.framework.container.AspectDef;
import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.impl.AspectDefImpl;

public class AnnotationAutoRegister extends AbstractAutoRegister {

	private Class annotationClass;

	private String interceptorName;

	@Override
	protected void regist(ComponentDef componentDef) {
		Class componentClass = componentDef.getComponentClass();
		if (componentClass == null) {
			return;
		}
		Pointcut pointcut = null;
		if (componentClass.isAnnotationPresent(annotationClass)) {
			pointcut = new PointcutImpl(componentClass);
		}
		if (pointcut == null) {
			Set<String> set = new HashSet<String>();
			for (Method method : componentClass.getMethods()) {
				if (method.isAnnotationPresent(annotationClass)) {
					set.add(method.getName());
				}
			}
			if (set.size() >= 1) {
				pointcut = new PointcutImpl(set.toArray(new String[set.size()]));
			}
		}
		if (pointcut != null) {
			AspectDef aspectDef = new AspectDefImpl(pointcut);
			aspectDef.setExpression(interceptorName);
			componentDef.addAspectDef(aspectDef);
			componentDef.destroy();
			componentDef.init();
		}
	}

	public Class getAnnotationClass() {
		return annotationClass;
	}

	public void setAnnotationClass(Class classObj) {
		this.annotationClass = classObj;
	}

	public String getInterceptorName() {
		return interceptorName;
	}

	public void setInterceptorName(String interceptorName) {
		this.interceptorName = interceptorName;
	}
}

AutoRegisterInitクラス

コンテナに登録されているAutoRegisterを探し出して初期化処理を行うクラス

package test;

import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.S2Container;

public class AutoRegisterInit {

	private S2Container container;

	public void init() {
		S2Container root = container.getRoot();
		initContainer(root);
		for (int i = 0; i < root.getChildSize(); i++) {
			initContainer(root.getChild(i));
		}
	}

	private void initContainer(S2Container ct) {
		for (int i = 0; i < ct.getComponentDefSize(); i++) {
			ComponentDef cd = ct.getComponentDef(i);
			Class componentClass = cd.getComponentClass();
			if (AutoRegister.class.isAssignableFrom(componentClass)) {
				String name = cd.getComponentName();
				AutoRegister obj = (AutoRegister) ct.getComponent(name);
				obj.registAll();
			}
		}
	}

	public S2Container getContainer() {
		return container;
	}

	public void setContainer(S2Container container) {
		this.container = container;
	}
}

AnnotationInterceptor

このクラスに指定したアノテーションを定義しているメソッドに対してAOPを行うMethodInterceptor。メソッドに定義が無くてもクラスに定義があれば実行する。このクラスによってオーバーロードされたメソッドの中からAOPする対象を選択できる。

package test;

import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public abstract class AnnotationInterceptor implements MethodInterceptor {

	private Class annotationClass;

	public Object invoke(MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();
		Class declaringClass = method.getDeclaringClass();
		if (declaringClass.isAnnotationPresent(annotationClass)
				|| method.isAnnotationPresent(annotationClass)) {
			return rootInvoke(invocation);
		}
		return invocation.proceed();
	}

	protected abstract Object rootInvoke(MethodInvocation invocation)
			throws Throwable;

	public Class getAnnotationClass() {
		return annotationClass;
	}

	public void setAnnotationClass(Class annotationClass) {
		this.annotationClass = annotationClass;
	}

}

ここまでが基盤となるクラス。ここで何か例を作ってみようということで、S2にある4種類のトランザクションを選択して実行出来る、Transactionアノテーションを作成してみました。

Transactionアノテーション

トランザクション設定を定義するアノテーション

package test;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Transaction {
	TransactionType value() default TransactionType.Required;
}

TransactionType Enum

トランザクションの種類を表すEnum

package test;

public enum TransactionType {
	Required, RequiresNew, Mandatory, NotSupported;
}

AnnotationTransactionInterceptor

Transactionアノテーションを定義しているメソッド、クラスに対して、トランザクション処理を行うMethodInterceptor

package test;

import java.lang.reflect.Method;
import java.util.Map;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class AnnotationTransactionInterceptor extends AnnotationInterceptor {
	
	private Map<TransactionType, MethodInterceptor> interceptorMap;

	public AnnotationTransactionInterceptor() {
		setAnnotationClass(Transaction.class);
	}
	@Override
	protected Object rootInvoke(MethodInvocation invocation) throws Throwable {
		Transaction annotation = null;
		Method method = invocation.getMethod();
		if (method.isAnnotationPresent(Transaction.class)) {
			annotation = method.getAnnotation(Transaction.class);
		} else {
			Class targetClass = method.getDeclaringClass();
			annotation = (Transaction) targetClass.getAnnotation(Transaction.class);
		}
		if (annotation != null) {
			TransactionType type = annotation.value();
			MethodInterceptor interceptor = interceptorMap.get(type);
			return interceptor.invoke(invocation);
		}
		return invocation.proceed();
	}

	public Map<TransactionType, MethodInterceptor> getInterceptorMap() {
		return interceptorMap;
	}

	public void setInterceptorMap(
			Map<TransactionType, MethodInterceptor> interceptorMap) {
		this.interceptorMap = interceptorMap;
	}

}

Actionインターフェイス

テスト用クラスのインターフェイス

package test;

public interface Action {
	void execute();
	void execute(String param);
}

Action1クラス

テスト用クラスその1。Actionインターフェイスを実装。

package test;

@Transaction(TransactionType.Required)
public class Action1 implements Action {

	public void execute() {
		System.out.println("Action1.execute()");
	}

	public void execute(String param) {
		System.out.println("Action1:" + param);
		
	}
	
	public void execute2() {
		System.out.println("Action1.execute2()");
	}

}

Action2クラス

テスト用クラスその2.インターフェイスは実装していない。execute(String param)メソッドにTransactionアノテーションを記述

package test;

public class Action2 {

	public void execute() {
		System.out.println("Action2.execute()");
	}

	@Transaction
	public void execute(String param) {
		System.out.println("Action2:" + param);		
	}
	
	public void execute2() {
		System.out.println("Action2.execute2()");
	}
}

aop.dicon

S2の設定ファイル

<?xml version="1.0" encoding="Windows-31J"?>
<!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN"
	"http://www.seasar.org/dtd/components21.dtd">
<components namespace="aop">
	<include path="j2ee.dicon"/>
	
	<!-- 初期化 -->
	<component name="autoRegisterInit" class="test.AutoRegisterInit"/>
	
	<!-- トランザクション自動AOP -->
	<component name="annotationAllTransactionRegister" class="test.AnnotationAutoRegister">
		<property name="annotationClass">@test.Transaction@class</property>
		<property name="interceptorName">"annotationAllTransaction"</property>
	</component>
	
	<!-- アノテーション対応トランザクションインターセプター -->
	<component name="annotationAllTransaction" class="test.AnnotationTransactionInterceptor">
		<property name="interceptorMap">
			#{@test.TransactionType@Required : j2ee.requiredTx,
			@test.TransactionType@RequiresNew : j2ee.requiresNewTx,
			@test.TransactionType@Mandatory : j2ee.mandatoryTx,
			@test.TransactionType@NotSupported : j2ee.notSupportedTx}
		</property>
	</component>
	
	<!-- Component -->
	<component class="test.Action1"/>
	<component class="test.Action2"/>
</components>

Mainクラス

実行クラス

package test;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;

public class Main {

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		S2Container container = S2ContainerFactory.create("test/aop.dicon");
		AutoRegisterInit arInit = (AutoRegisterInit) container.getComponent(AutoRegisterInit.class);
		arInit.init();
		
		Action1 action1 = (Action1) container.getComponent(Action1.class);
		Action2 action2 = (Action2) container.getComponent(Action2.class);
		
		action1.execute();
		action1.execute("Hello");
		action1.execute2();
		action2.execute();
		action2.execute("Hello");
		action2.execute2();
	}

}

実行結果

DEBUG 2005-09-01 08:14:28,046 [main] トランザクションを開始しました
Action1.execute()
DEBUG 2005-09-01 08:14:28,046 [main] トランザクションをコミットしました
DEBUG 2005-09-01 08:14:28,046 [main] トランザクションを開始しました
Action1:Hello
DEBUG 2005-09-01 08:14:28,046 [main] トランザクションをコミットしました
Action1.execute2()
Action2.execute()
DEBUG 2005-09-01 08:14:28,046 [main] トランザクションを開始しました
Action2:Hello
DEBUG 2005-09-01 08:14:28,046 [main] トランザクションをコミットしました
Action2.execute2()

Action1はクラスにTransactionアノテーションを定義したので、Actionインターフェイスの実装メソッドについてトランザクションがかかり、Action2はexecute(String param)メソッドのみにトランザクションがかかりました。これで、アノテーションを定義したメソッドにAOPをかけることが出来るようになり、オーバーロードしたメソッドも選択できるようになりました。
こういう自作クラスを作らなくても、Seasar4でこういう機能が提供されるんでしょうけど、現行のS2で、設定ファイルの記述量を少しでも減らしたいと思っているときは、このような自作クラスで工夫できるのではないかと思いました。アノテーションについても今回初めて勉強したのですが、リフレクションを使って様々なデータを取得できるところが面白いですね。設定ファイルがほとんど無くなって、アノテーションで自由自在にアスペクトの定義が出来るようになれば、DIコンテナもこれから更に面白くなっていきそうな気がします。