Seasar2.3その5

開発中のプロジェクトの環境に入れるかどうかは別として、入れたときに欲しい機能を色々と考えてました。
まずはJarファイル上のコンポーネントへの自動設定機能。ローカルアプリ用に作ったコンポーネント群をjarにまとめてWebコンテナ上で使う・・・みたいな状況を想定しています。jara.util.jar.JarFileを軽く調べたところ、

  • entries()メソッドで中身のファイル群を一気に取得できる
  • ディレクトリ構成は「packagename1/packagename2/ClassName.class」みたいな形のStringで取得できる。

ということがわかりました。WindowsXP上のJDK1.5でしか試してないのですが、まぁ細かいことは取りあえず置いといて・・・
FileSystemComponentAutoRegisterを参考にして、ある特定のJarFileの中を調べてコンポーネントの設定を行う、JarComponentAutoRegisterを作ってみました。

package test.interceptor;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.servlet.ServletContext;

import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.autoregister.AutoNaming;
import org.seasar.framework.container.autoregister.AutoRegister;
import org.seasar.framework.container.autoregister.ClassPattern;
import org.seasar.framework.container.factory.AnnotationHandler;
import org.seasar.framework.container.factory.AnnotationHandlerFactory;

public class JarComponentAutoRegister extends AutoRegister {

	private static final String CLASS_SUFFIX = ".class";

	private String jarName;
	
	private ServletContext context;

	private AutoNaming autoNaming;

	public void setJarName(String jarName) {
		this.jarName = jarName;
	}
	
	public void setContext(ServletContext context) {
		this.context = context;
	}

	public void setAutoNaming(AutoNaming autoNaming) {
		this.autoNaming = autoNaming;
	}

	public void registAll() throws IOException, URISyntaxException {
		for (int i = 0; i < getClassPatternSize(); ++i) {
			ClassPattern cp = getClassPattern(i);
			regist(cp);
		}
	}

	protected void regist(ClassPattern classPattern) throws IOException, URISyntaxException {
		JarFile jar = getJarFile(jarName);
		for (Enumeration<JarEntry> en = jar.entries(); en.hasMoreElements();) {
			JarEntry jarEntry = en.nextElement();
			regist(classPattern, jarEntry);
		}
	}


	protected void regist(ClassPattern classPattern, JarEntry jarEntry) {
		AnnotationHandler annoHandler = AnnotationHandlerFactory
				.getAnnotationHandler();
		
		String fileName = jarEntry.getName();
		if (fileName.endsWith(CLASS_SUFFIX)) {
			String packageName = null;
			
			int lastIndex = fileName.lastIndexOf("/");
			if (lastIndex == -1) {
				lastIndex = 0;
			} else {
				lastIndex++;
				packageName = fileName.substring(0, fileName.lastIndexOf("/")).replace('/', '.');
			}
			String shortClassName = fileName.substring(lastIndex, fileName.length() - CLASS_SUFFIX.length());
			if (isIgnore(packageName, shortClassName)) {
				return;
			}
			if (isPackageApplied(classPattern.getPackageName(), packageName)
				&& classPattern.isApplied(shortClassName)) {
				String className = packageName == null ? shortClassName
						: packageName + "." + shortClassName;
				ComponentDef cd = annoHandler.createComponentDef(className);
				if (cd.getComponentName() == null && autoNaming != null) {
					cd.setComponentName(autoNaming.defineName(packageName,
							shortClassName));
				}
				if (hasComponentDef(cd.getComponentName())) {
					return;
				}
				annoHandler.appendDI(cd);
				getContainer().register(cd);
			}
			
			
		}
	}

	protected JarFile getJarFile(String jarName) throws IOException, URISyntaxException {
		if (jarName == null) {
			throw new RuntimeException("jarファイルが定義されてません");
		}
		if (context != null) {
			URL url = context.getResource(jarName);
			File file = new File(url.toURI());
			return new JarFile(file);
		}
		String classPath = System.getProperty("java.class.path");
		String[] cps = classPath.split(";");
		for (String cp : cps) {
			if (cp.endsWith(jarName)) {
				return new JarFile(cp);
			}
		}
		throw new RuntimeException("CLASSPATHに" + jarName + "は定義されてません");
	}
	
	protected boolean isPackageApplied(String expected, String packageName) {
		if (expected == null) {
			return true;
		}
		if (packageName == null) {
			return false;
		}
		if (expected.equals(packageName)) {
			return true;
		}
		return packageName.startsWith(expected) && ".".equals(packageName.substring(expected.length(), expected.length() + 1));
	}

}

Web上ではServletContext.getResource()でJarを指定、ローカルアプリの場合はCLASSPATHからJarの置き場所を調べるようなかんじで作ってみました。ちと強引過ぎる気もしますが・・・
さて、次はAspectの設定方法について。今の開発環境では、interceptorと特定のアノテーションを関連付けて、そのアノテーションを定義した場所に対してアスペクトが適用されるように作ってます。こうすることによってpointcutでオーバーロードメソッドに対応出来ない問題が解決出来るし、見た目にもわかりやすくなるのではないかと思ったからです。EJB3が広まれば、そのアノテーションだけ使ってクラスに定義して、実際の挙動はS2のアスペクトで実行・・・みたいなことも出来そうですし。
というわけで、アスペクトについてはクラスパターンで定義するより「S2Containerに登録されている全てのコンポーネントを調べて、アノテーションが付いてるものに対してAspectの定義を行う」みたいな機能が欲しいです。これは前々から作っていたものがあるので、それを利用します。

package test.interceptor;

public interface AutoAllRegister {
	void registAll();
}
package test.interceptor;

import java.util.HashSet;
import java.util.Set;

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

public abstract class AbstractAutoAllRegister implements AutoAllRegister {

	private S2Container container;
	
	public void registAll() {
		Set<S2Container> set = new HashSet<S2Container>();
		S2Container root = container.getRoot();
		registContainer(root, set);
	}

	private void registContainer(S2Container container, Set<S2Container> set) {
		if (!set.contains(container)) {
			for (int i = 0; i < container.getComponentDefSize(); i++) {
				ComponentDef cd = container.getComponentDef(i);
				regist(cd);
			}
			for (int i = 0; i < container.getChildSize(); i++) {
				registContainer(container.getChild(i), set);
			}
			set.add(container);
		}
	}

	protected abstract void regist(ComponentDef componentDef);

	public S2Container getContainer() {
		return container;
	}

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

import java.lang.annotation.Annotation;
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 AnnotationAspectAutoAllRegister extends AbstractAutoAllRegister {

	private Class<? extends Annotation> annotationClass;

	private String interceptorName;

	public void setAnnotationClass(Class<? extends Annotation> annotationClass) {
		this.annotationClass = annotationClass;
	}

	public void setInterceptorName(String interceptorName) {
		this.interceptorName = 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);
		}
	}

}

名前とか細かいところをちょこっと変更しただけです。
・・・で、これを利用するには、S2Containerに自動設定も含めてコンポーネントが全て登録されている必要があります。というわけで、コンポーネント関連のAutoRegisterを抜き出して初期化処理を行うクラスと、自作のAspectAutoAllRegisterの初期化処理を行うクラスを作成

package test.interceptor;

import java.io.IOException;
import java.net.URISyntaxException;

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

public class ComponentRegisterInit extends AbstractAutoAllRegister {
	

	@Override
	protected void regist(ComponentDef componentDef) {
		Class<?> componentClass = componentDef.getComponentClass();
		String name = componentDef.getComponentName();
		if (FileSystemComponentAutoRegister.class.isAssignableFrom(componentClass)) {
			FileSystemComponentAutoRegister obj = (FileSystemComponentAutoRegister) componentDef.getContainer().getComponent(name == null ? componentClass : name);
			obj.registAll();
		} else if (	JarComponentAutoRegister.class.isAssignableFrom(componentClass)) {
			JarComponentAutoRegister obj = (JarComponentAutoRegister) componentDef.getContainer().getComponent(name == null ? componentClass : name);
			try {
				obj.registAll();
			} catch (IOException e) {
				throw new RuntimeException(e);
			} catch (URISyntaxException e) {
				throw new RuntimeException(e);
			}
		}
		
	}

}
package test.interceptor;

import org.seasar.framework.container.ComponentDef;

public class AspectRegisterInit extends AbstractAutoAllRegister {

	@Override
	protected void regist(ComponentDef componentDef) {
		Class<?> componentClass = componentDef.getComponentClass();
		if (AnnotationAspectAutoAllRegister.class.isAssignableFrom(componentClass)) {
			String name = componentDef.getComponentName();
			AnnotationAspectAutoAllRegister obj = (AnnotationAspectAutoAllRegister) componentDef.getContainer().getComponent(name == null ? componentClass : name);
			obj.registAll();
		} 
	}

}

・・・クラス名と初期化メソッドを指定して、リフレクションで作るようにした方がいい気がしますが、取りあえず強引に作ってみました。
・・・で、サンプル作成。ここはJar環境ということで、Tomcat5.5上で動かしてみたいと思います。id:da-yoshi:20051014#1129335707で作ったサンプルのTestClientのgetMessageメソッドの戻り値を、voidからStringを返すように修正。そしてtest.jarという名前でjarにまとめます。
それらをWeb上で呼び出すServletを作成。

package test.web;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

import test.client.TestClient;

@SuppressWarnings("serial")
public class TestServlet extends HttpServlet {

	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		S2Container container = SingletonS2ContainerFactory.getContainer();
		TestClient client1 = (TestClient) container.getComponent("test1.impl1");
		TestClient client2 = (TestClient) container.getComponent("test2.impl2");
		response.setContentType("text/plain; charset=Windows-31J");
		PrintWriter out = response.getWriter();
		out.println(client1.execute());
		out.println(client2.execute());
	}

	
}

web.xmlに登録

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4">

  <filter>
    <filter-name>s2filter</filter-name>
    <filter-class>org.seasar.framework.container.filter.S2ContainerFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>s2filter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>s2container</servlet-name>
    <servlet-class>org.seasar.framework.container.servlet.S2ContainerServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  
  <servlet>
    <servlet-name>test</servlet-name>
    <servlet-class>test.web.TestServlet</servlet-class>
    <load-on-startup>2</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>s2container</servlet-name>
    <url-pattern>/s2container</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>test</servlet-name>
    <url-pattern>/test</url-pattern>
  </servlet-mapping>

</web-app>

test.jarはS2関連ライブラリと一緒にWEB-INF/libに配置。次はdiconですね。
まずは初期化処理を行うinit.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="init">

    <component name="traceRegister"
		class="test.interceptor.AnnotationAspectAutoAllRegister">
		<property name="annotationClass">@test.annotation.Trace@class</property>
		<property name="interceptorName">"aop2.annotationTraceInterceptor"</property>
	</component>

	<component name="transactionRegister"
		class="test.interceptor.AnnotationAspectAutoAllRegister">
		<property name="annotationClass">@test.annotation.Transaction@class</property>
		<property name="interceptorName">"aop2.annotationTransactionInterceptor"</property>
	</component>

    <component name="componentRegisterInit"
    	class="test.interceptor.ComponentRegisterInit">
    	<initMethod name="registAll"/>
    </component>
    
    <component name="aspectRegisterInit"
    	class="test.interceptor.AspectRegisterInit">
    	<initMethod name="registAll"/>
    </component>
    
</components>

impl1.dicon、impl2.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"/>

	<component
  		class="test.interceptor.JarComponentAutoRegister">
  		<property name="jarName">"/WEB-INF/lib/test.jar"</property>
    	<property name="autoNaming">
    		<component class="org.seasar.framework.container.autoregister.DefaultAutoNaming"/>
    	</property>
    	<initMethod name="addClassPattern">
    		<arg>"test.impl"</arg>
    		<arg>".*Impl"</arg>
    	</initMethod>
    	<initMethod name="addClassPattern">
    		<arg>"test.client.impl"</arg>
    		<arg>".*Impl"</arg>
    	</initMethod>
    </component>
        
</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"/>

	<component
  		class="test.interceptor.JarComponentAutoRegister">
  		<property name="jarName">"/WEB-INF/lib/test.jar"</property>
    	<property name="autoNaming">
    		<component class="org.seasar.framework.container.autoregister.DefaultAutoNaming"/>
    	</property>
    	<initMethod name="addClassPattern">
    		<arg>"test.impl2"</arg>
    		<arg>".*Impl"</arg>
    	</initMethod>
    	<initMethod name="addClassPattern">
    		<arg>"test.client.impl2"</arg>
    		<arg>".*Impl"</arg>
    	</initMethod>
    </component>
    
</components>

アスペクト関連の定義を外しました。初期化を行うinitMethodタグも外してます。
最後にapp.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>
	<include path="impl1.dicon"/>
	<include path="impl2.dicon"/>
    
    <include path="init.dicon"/>
</components>

これをTomcatに配置し、mappingに指定したURLにアクセス

TestClientImpl:こんにちは
TestClient2Impl:Hello

そのとき出たログ

DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] トランザクションを開始しました
DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] BEGIN test.impl.TestImpl#getMessage2()
DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] END test.impl.TestImpl#getMessage2() : こんにちは
DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] トランザクションをコミットしました
DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] BEGIN test.client.impl2.TestClient2Impl#execute()
DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] トランザクションを開始しました
DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] トランザクションをコミットしました
DEBUG 2005-10-16 10:55:59,718 [http-8080-Processor24] END test.client.impl2.TestClient2Impl#execute() : TestClient2Impl:Hello

・・・とりあえず成功かな? JBossのようにwarにまとめて配置する必要がある場合はまた考えないといけないのかもしれませんが・・・取りあえずTomcatで動けば自分的には要件を満たすので満足です。
さて、プロジェクトはどうしようか・・・いつ正式版が出るかによって変わってくるし・・・