Seasar2.3でEJB3アノテーションを使ってみる

なんだかS2の中身を勉強するのにハマってしまいました。元々仕事で使うという理由で真面目に色々調べていたのですが、仕事の方は2.2を採用するということで決着が付いたので、今はプライベートの時間に趣味で色々調べてます。
前から書いてるように、自分的なEoD環境の理想は「EJB3のようにアノテーションを主体とした定義で開発が出来て、且つDIコンテナのように簡単で環境を選ばない仕組みで動かせる」ことです。アノテーションとDIコンテナとの相性は凄く良くて、使えば使うほど「あんなことや、こんなことも出来るんじゃないか?」という想像がどんどん湧いてきちゃいます。もう今までの開発環境には戻れないかも・・・
さて、今回は、「現在diconファイル上で行っているコンテナ定義の分割とネームスペース指定もアノテーションでやってしまおう」、ということに挑戦したいと思います。これが成功すれば、基盤系以外のdiconファイルは、app.diconの分割の定義と、分割ファイルの中のinclude定義以外必要無くなる予定です。
まずは、自分がどのネームスペースに属するかを表すComponentsアノテーションを作ってみました。

package test.annotation;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Components {
	String namespace();
}

このアノテーションを定義すれば、「初期化処理で、Componentsで定義したネームスペースのコンテナに自動登録してくれる」という動作を期待しています。
また、コンポーネント自動設定の検索も、クラスパターンで細かく指定するのではなく、「特定のアノテーションを定義しているクラス」に対して漏れなく行えるようにしたいと思います。
以上の2点を実現する為に、AbstractComponentAutoRegisterのregistメソッドを置き換える為のユーティリティクラスを作ってみました。

package test.register;

import org.seasar.framework.container.InstanceDef;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.autoregister.AutoNaming;

public interface AnnotationComponentAutoRegisterUtil {

	public abstract void regist(String packageName, String shortClassName,
			InstanceDef instanceDef, AutoNaming autoNaming,
			S2Container container);

	public abstract S2Container getContainer(S2Container container,
			String namespace);

}
package test.register;

import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Set;

import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.InstanceDef;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.autoregister.AutoNaming;
import org.seasar.framework.container.factory.AnnotationHandler;
import org.seasar.framework.container.factory.AnnotationHandlerFactory;
import org.seasar.framework.util.ClassUtil;

import test.annotation.Components;

public class AnnotationComponentAutoRegisterUtilImpl implements AnnotationComponentAutoRegisterUtil {
	
	private Set<Class<? extends Annotation>> annotationSet;
	
	public void setAnnotationSet(Set<Class<? extends Annotation>> annotationSet) {
		this.annotationSet = annotationSet;
	}


	/* (非 Javadoc)
	 * @see test.register.AnnotationComponentAutoRegisterUtil#regist(java.lang.String, java.lang.String, org.seasar.framework.container.InstanceDef, org.seasar.framework.container.autoregister.AutoNaming, org.seasar.framework.container.S2Container)
	 */
	public void regist(String packageName, String shortClassName,
			InstanceDef instanceDef, AutoNaming autoNaming, S2Container container) {
		final AnnotationHandler annoHandler = AnnotationHandlerFactory
				.getAnnotationHandler();
		final String className = ClassUtil.concatName(packageName,
				shortClassName);

		Class<?> clazz = ClassUtil.forName(className);
		for (Class<? extends Annotation> annotationClass : annotationSet) {
			if (clazz.isAnnotationPresent(annotationClass)) {
				final ComponentDef cd = annoHandler.createComponentDef(className,
						instanceDef);
				if (cd.getComponentName() == null && autoNaming != null) {
					cd.setComponentName(autoNaming.defineName(packageName,
							shortClassName));
				}
				annoHandler.appendDI(cd);

				String namespace = null;
				if (clazz.isAnnotationPresent(Components.class)) {
					namespace = clazz.getAnnotation(Components.class).namespace();
				}
				getContainer(container, namespace).register(cd);
				return;
			}
		}
	}

	/* (非 Javadoc)
	 * @see test.register.AnnotationComponentAutoRegisterUtil#getContainer(org.seasar.framework.container.S2Container, java.lang.String)
	 */
	public S2Container getContainer(S2Container container, String namespace) {
		Set<S2Container> set = new HashSet<S2Container>();
		container = container.getRoot();
		if (namespace == null || checkNamespace(container, namespace)) {
			return container;
		}
		return searchChild(container, namespace, set);
	}

	private S2Container searchChild(S2Container container,
			String namespace, Set<S2Container> set) {
		if (set.contains(container)) {
			return null;
		}
		S2Container ret = null;
		for (int i = 0; i < container.getChildSize(); i++) {
			S2Container child = container.getChild(i);
			if (checkNamespace(child, namespace)) {
				ret = child;
			}
			if (ret == null) {
				ret = searchChild(child, namespace, set);
			}
			if (ret != null) {
				return ret;
			}
		}
		set.add(container);
		return null;
	}

	private boolean checkNamespace(S2Container container,
			String namespace) {
		return namespace.equals(container.getNamespace());

	}

}

annotationSetの中に、Containerに登録する目印となるアノテーションをセットしておきます。また、登録する際にComponentsアノテーションのネームスペース値を調べて、そのネームスペースを持つContainerを探しに行きます。なんかソースがあまり美しくないですが、まぁいいか・・・
このユーティリティのdicon設定はこちら

<component
		class="test.register.AnnotationComponentAutoRegisterUtilImpl">
		<property name="annotationSet">
			<component class="java.util.HashSet">
				<initMethod>#self.add(@org.seasar.framework.container.annotation.tiger.Component@class)</initMethod>
				<initMethod>#self.add(@javax.ejb.Stateless@class)</initMethod>
				<initMethod>#self.add(@javax.ejb.Stateful@class)</initMethod>
			</component>
		</property>
</component>

アノテーションは何でもいいんですが、Jboss-EJB3からライブラリを持ってきて、EJB3アノテーションを使ってみました。なんか妙に贅沢感を感じます(笑)
で、このユーティリティを使ってFileSystemComponentAutoRegisterを拡張します。

package test.register;

import org.seasar.framework.container.autoregister.FileSystemComponentAutoRegister;

public class AnnotationFileSystemComponentAutoRegister extends
		FileSystemComponentAutoRegister {
	
	private AnnotationComponentAutoRegisterUtil util;

	public AnnotationFileSystemComponentAutoRegister(AnnotationComponentAutoRegisterUtil util) {
		this.util = util;
	}

	@Override
	protected void regist(String packageName, String shortClassName) {

		util.regist(packageName, shortClassName, getInstanceDef(),
				getAutoNaming(), getContainer());

	}
}

JarComponentAutoRegisterも同様に拡張できる筈です(今回は試してませんが)
で、このRegisterのdiconファイル設定は

<component
  		class="test.register.AnnotationFileSystemComponentAutoRegister">
  		<property name="fileNameOfRoot">"app.dicon"</property>
    	<property name="autoNaming">
    		<component class="org.seasar.framework.container.autoregister.DefaultAutoNaming"/>
    	</property>
    	<initMethod name="addClassPattern">
    		<arg>"test"</arg>
    		<arg>".*"</arg>
    	</initMethod>
    	<initMethod name="registAll"/>
</component>

自作パッケージの全てのクラスを一気に検索します。以前作った、初期化用のComponentRegisterInitはもういらなくなりました。
・・・すると、それぞれの個別diconファイルは以下のようになる筈

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC
    "-//SEASAR//DTD S2Container 2.3//EN"
    "http://www.seasar.org/dtd/components23.dtd">

<components namespace="test1">

	<include path="aop2.dicon"/>
    
</components>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC
    "-//SEASAR//DTD S2Container 2.3//EN"
    "http://www.seasar.org/dtd/components23.dtd">

<components namespace="test2">

	<include path="aop2.dicon"/>
    
</components>

ネームスペースとincludeの設定以外無くなってしまいました。
これで早速テスト・・・といきたいのですが、折角だからトランザクション用のアノテーションも、EJB3のものに変えてしまいたいと思います。
昔作った、トランザクション用のインターセプターをちょっと改造してみました。

package test.interceptor;

import java.util.Map;

import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

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

public class AnnotationTransactionInterceptor extends AnnotationInterceptor {

	private Map<TransactionAttributeType, MethodInterceptor> interceptorMap;

	public AnnotationTransactionInterceptor() {
		setAnnotationClass(TransactionAttribute.class);
	}

	@Override
	protected Object rootInvoke(MethodInvocation invocation) throws Throwable {
		TransactionAttribute annotation = (TransactionAttribute) getAnnotation(invocation);
		if (annotation != null) {
			TransactionAttributeType type = annotation.value();
			MethodInterceptor interceptor = interceptorMap.get(type);
			return interceptor.invoke(invocation);
		}
		return invocation.proceed();
	}

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

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

}

これを定義したdiconファイルがこれ

    <component name="annotationTransactionInterceptor"
    	class="test.interceptor.AnnotationTransactionInterceptor">
    	<property name="interceptorMap">
    	#{@javax.ejb.TransactionAttributeType@REQUIRED : j2ee.requiredTx,
    	@javax.ejb.TransactionAttributeType@REQUIRES_NEW : j2ee.requiresNewTx,
    	@javax.ejb.TransactionAttributeType@MANDATORY : j2ee.mandatoryTx,
    	@javax.ejb.TransactionAttributeType@NOT_SUPPORTED : j2ee.notSupportedTx}
    	</property>
    	<property name="annotationClass">@javax.ejb.TransactionAttribute@class</property>
    </component>    

別にEJBの動きをシミュレーションするつもりは無いので、一般的なトランザクションのインターセプターを関連づけてみました。
・・・最後に、テスト用クラスのアノテーションを変更

package test.client.impl;

import org.seasar.framework.container.annotation.tiger.Component;

import test.Test;
import test.annotation.Components;
import test.client.TestClient;

@Components(namespace = "test1")
@Component(name = "client")
public class TestClientImpl implements TestClient {
	
	private Test test;

	public void setTest(Test test) {
		this.test = test;
	}
	
	public String execute() {
		return "TestClientImpl:" + test.getMessage();

	}

}
package test.client.impl2;

import org.seasar.framework.container.annotation.tiger.Component;

import test.Test;
import test.annotation.Components;
import test.annotation.Trace;
import test.client.TestClient;

@Components(namespace = "test2")
@Component(name = "client")
public class TestClient2Impl implements TestClient {

	private Test test;

	public void setTest(Test test) {
		this.test = test;
	}
	
	@Trace
	public String execute() {
		return "TestClient2Impl:" + test.getMessage();

	}

}
package test.impl;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;

import test.Test;
import test.annotation.Components;
import test.annotation.Trace;

@Components(namespace = "test1")
@Stateless
public class TestImpl implements Test {

	@TransactionAttribute
	public String getMessage() {
		return getMessage2();
	}

	@Trace
	public String getMessage2() {
		return "こんにちは";
	}

}
package test.impl2;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

import test.Test;
import test.annotation.Components;

@Components(namespace = "test2")
@Stateless
public class Test2Impl implements Test {

	@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
	public String getMessage() {
		return "Hello";
	}

}

テスト用に、呼び出す名前を同じにして、ネームスペースだけ違うようになるよう変更しました。クライアントは名前の定義が要るので@Componentで、Testオブジェクトは適当に@Statelessをつけてみました(別にSessionBeanになるわけじゃありませんがw)。また、トランザクションアノテーションは、EJB3の@TransactionAttributeに変更してます。
・・・以上で準備完了。実行してみると・・・

DEBUG 2005-10-19 03:19:20,281 [main] トランザクションを開始しました
DEBUG 2005-10-19 03:19:20,281 [main] BEGIN test.impl.TestImpl#getMessage2()
DEBUG 2005-10-19 03:19:20,281 [main] END test.impl.TestImpl#getMessage2() : こんにちは
DEBUG 2005-10-19 03:19:20,281 [main] トランザクションをコミットしました
TestClientImpl:こんにちは

DEBUG 2005-10-19 03:19:20,312 [main] BEGIN test.client.impl2.TestClient2Impl#execute()
DEBUG 2005-10-19 03:19:20,328 [main] トランザクションを開始しました
DEBUG 2005-10-19 03:19:20,328 [main] トランザクションをコミットしました
DEBUG 2005-10-19 03:19:20,328 [main] END test.client.impl2.TestClient2Impl#execute() : TestClient2Impl:Hello
TestClient2Impl:Hello

あんまり細かいことは試してませんが、どうやら今まで通り動いたっぽい。これでコンポーネント定義もアスペクト定義もアノテーション主体で定義できるようになり、自作アノテーションEJB3のようなメジャーなアノテーションも好きに使えるってことがわかりました。EJB3をもっと勉強して同じ動きになるように修正していけば、J2EEサーバが出るのを待たずとも、EJB3モドキの環境がDIコンテナ上で動かせるようになるかも?
まぁEJB3の中で強く興味があるのはEntityBeanぐらいなので、それ以外の機能が実現出来てもあんまし意味無いかもしれませんが・・・