TopLink EssentialsをTomcatで動かす その2

http://d.hatena.ne.jp/da-yoshi/20060709/1152458525の続きです。
前回、Tomcat上でTopLink Essentialsを動かしてみたのですが、この方法だとN対1のLAZYロードが有効になりません。これを有効にする方法を試行錯誤していたのですが、なかなか上手くいきませんでした。
どうやら、JPAのPersistenceUnitInfo、ClassTransformer、PersistenceProvider.createContainerEntityManagerFactory等のAPIは、TopLinkのクラスエンハンス機能の為に用意されたものみたいですね。Hibernateの場合は、これらのAPIを使わなくてもクラスエンハンス機能を利用することが出来ます。
具体的には・・・

  • Entityの一時的なロード用のクラスローダ(PersistenceUnitInfo.getNewTempClassLoader())と、実際にEntityをロードするクラスローダ(PersistenceUnitInfo.getClassLoader())の2つのクラスローダを用意する。
  • 一時ロード用クラスローダを利用してEntityのアノテーション情報を取得し、クラスをエンハンスするClassTransformerオブジェクトを作成。
  • 作成したClassTransformerをPersistenceUnitInfoに返す(PersistenceUnitInfo.addTransformer())。
  • コンテナ側は渡されたClassTransformerを使ってEntityクラスをロードする。

この環境を用意するのが難しかったです。まず、Entityロード用クラスローダと一時利用用のクラスローダは親子関係にならないようにならなければいけないし、共通の親クラスローダからEntityがロード出来てしまうとコントロールがかなり難しくなります。よって、APサーバが提供するクラスローダを拡張して、ClassTransformerをセットできるようにする方法を取ることにしました。Tomcatの場合はWebAppClassLoaderですね。これを拡張することからはじめたいと思います。
まずはClassLoaderが実装するインターフェイスを作成(パッケージ名にSeasarを使ってしまってますが、まだ申請前なので勝手に名前をつけてる状態になってます。まずけれは修正します)。

package org.seasar.toplink.jpa.classloader;

import javax.persistence.spi.ClassTransformer;


public interface JpaClassLoader {

	void addTransformer(ClassTransformer transformer);
	
	ClassLoader getNewTempClassLoader();
}

ClassTransformerを取得するaddTransformerと、一時的クラスローダを作成するgetNewTempClassLoaderを持ちます。
続いて、上記インターフェイスを実装し、WebAppClassLoaderを継承するTomcat用実装クラス。

package org.seasar.toplink.jpa.classloader.tomcat;

import java.lang.instrument.IllegalClassFormatException;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.spi.ClassTransformer;

import org.apache.catalina.loader.ResourceEntry;
import org.apache.catalina.loader.WebappClassLoader;
import org.seasar.toplink.jpa.classloader.JpaClassLoader;

public class JpaWebAppClassLoader extends WebappClassLoader implements
		JpaClassLoader {

	private List<ClassTransformer> classTransformerList = new ArrayList<ClassTransformer>();

	public JpaWebAppClassLoader() {
	}

	public JpaWebAppClassLoader(ClassLoader parent) {
		super(parent);
	}

	public void addTransformer(ClassTransformer transformer) {
		classTransformerList.add(transformer);
	}

	@Override
	protected ResourceEntry findResourceInternal(String name, String path) {
		ResourceEntry re = super.findResourceInternal(name, path);
		if (re != null && re.loadedClass == null && re.binaryContent != null) {
			for (ClassTransformer ct : classTransformerList) {
				try {
					byte[] b = ct.transform(this, name.replace('.', '/'), null, null,
							re.binaryContent);
					if (b != null) {
						re.binaryContent = b;
					}
				} catch (IllegalClassFormatException e) {
					log.error(e.getMessage(), e);
				}
			}
		}
		return re;
	}

	@Override
	protected void clearReferences() {
		super.clearReferences();
		classTransformerList.clear();
	}
	
	public ClassLoader getNewTempClassLoader() {
		return new URLClassLoader(getURLs(), getParent());
	}
}

このクラスを登録するserver.xml

<Context path="/TopLinkWeb" reloadable="true" docBase="I:\workspace\TopLinkWeb\WebContent" workDir="I:\workspace\TopLinkWeb\work">
	<Logger className="org.apache.catalina.logger.SystemOutLogger" verbosity="4" timestamp="true"/>
	<Loader loaderClass="org.seasar.toplink.jpa.classloader.tomcat.JpaWebAppClassLoader"/>
	<Resource name="jdbc.DataSource" auth="Container"
      		type="javax.sql.DataSource"
      		factory="org.seasar.toplink.jpa.DataSourceFactory"/>
</Context>

当初はWTPで動かそうとしていたのですが、どうやらWTPではClassLoaderを変えるとTomcatが正常動作しないみたいです・・・やむなくTomcatPluginに環境を戻しました。
TomcatのWebAppClassLoaderがクラス情報をバイト配列で取得した後に、ClassTransformerのメソッドをかませてバイト配列を変換して登録します。インターフェイスクラスを持つJarを%TOMCAT_HOME%/commons/libに、実装クラスを持つJarを%TOMCAT_HOME%/server/libに配置しました。
クラスは一度登録したら変更出来ないので(もしかして出来るのかもしれませんが、今の自分にはよくわかってませんorz)、ClassTransformerを渡される前にEntityクラスをロードしてしまうと上手く動きません。ここの動きにかなりてこずって、どうしようか迷っていたのですが・・・結局、S2Containerを初期化する前にEntityManagerFactoryを作成し、作成したEntityManagerFactoryをS2側が再利用する形で作ってみました。
以下はS2ContainerServletを継承した、初期化用クラスです。

package org.seasar.toplink.jpa;

import java.io.IOException;
import java.util.Scanner;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.xml.parsers.ParserConfigurationException;

import org.seasar.framework.container.servlet.S2ContainerServlet;
import org.xml.sax.SAXException;

public class JpaS2ContainerServlet extends S2ContainerServlet {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 632043324803314008L;

	@Override
	public void init() {
//		String configPath = getServletConfig().getInitParameter("persistenceConfigPath");
//		if (configPath == null) {
//			configPath = "jpa.dicon";
//		}
//		S2Container container = S2ContainerFactory.create(configPath);
//		PersistenceUnitManager pum = (PersistenceUnitManager) container.getComponent(PersistenceUnitManager.class);
		try {
			ContainerPersistenceUnitManager pum = new ContainerPersistenceUnitManager();
			pum.setFactoryFactory(new ContainerEntityManagerFactoryFactoryImpl(
				new InitialContext()));
			
			String puNames = getServletConfig().getInitParameter("persistenceUnitName");
			if (puNames != null) {
				Scanner s = new Scanner(puNames).useDelimiter(",");
				while (s.hasNext()) {
					pum.getEntityManagerFactory(s.next());
				}
			}
			super.init();
		} catch (ParserConfigurationException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		} catch (SAXException e) {
			throw new RuntimeException(e);
		} catch (NamingException e) {
			throw new RuntimeException(e);
		}
	}


}

DI的に作れないのが気持ち悪いのですが、S2Containerを使った場合、closeすると作成したEntityManagerFactoryをcloseしてしまうので・・・結局ベタ書きで書いてしまいました。
そして、PersistenceProvider.createContainerEntityManagerFactoryメソッドを実行するクラス群を作成。

public class ContainerEntityManagerFactoryFactoryImpl implements
		ContainerEntityManagerFactoryFactory {

	private Map<String, PersistenceUnitInfo> puiMap = new HashMap<String, PersistenceUnitInfo>();

	private Map<String, PersistenceProvider> providers = new HashMap<String, PersistenceProvider>();

	public ContainerEntityManagerFactoryFactoryImpl(Context context)
			throws ParserConfigurationException, IOException, SAXException,
			NamingException {
		Enumeration<URL> xmls = Thread.currentThread()
			.getContextClassLoader()
			.getResources("META-INF/persistence.xml");

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();

		while (xmls.hasMoreElements()) {
			URL url = xmls.nextElement();
			BufferedInputStream in = new BufferedInputStream(url.openStream());

			try {
				Document doc = builder.parse(in);
				NodeList puList = doc.getElementsByTagName("persistence-unit");
				for (int i = 0; i < puList.getLength(); i++) {
					Element element = (Element) puList.item(i);
					puiMap.put(element.getAttribute("name"),
							new PersistenceUnitInfoImpl(
									element,
									context,
									getPersistenceUnitRootUrl(url)));
				}

			} finally {
				in.close();
			}
		}
		findAllProviders();

	}

	public PersistenceUnitInfo getPersistenceUnitInfo(String persistenceUnitName) {
		return puiMap.get(persistenceUnitName);
	}

	public EntityManagerFactory getContainerEntityManagerFactory(
			String persistenceUnitName, Map map) {
		PersistenceUnitInfo info = puiMap.get(persistenceUnitName);
		PersistenceProvider provider = providers.get(
				info.getPersistenceProviderClassName());
		if (provider != null) {
			return provider.createContainerEntityManagerFactory(info, map);
		} else {
			for (String key : providers.keySet()) {
				provider = providers.get(key);
				EntityManagerFactory factory = provider.createContainerEntityManagerFactory(info, map);
				if (factory != null) {
					return factory;
				}
			}
		}
		return null;
	}
(後略)

PersistenceUnitManagerを継承したクラスが上記のクラスを利用してEntityManagerFactoryを作成します。中身は省略。。。
Providerに渡すPersistenceUnitInfoの実装クラス(まだ作りかけですが。。。)

package org.seasar.toplink.jpa;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.persistence.spi.ClassTransformer;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.persistence.spi.PersistenceUnitTransactionType;
import javax.sql.DataSource;

import org.seasar.toplink.jpa.classloader.JpaClassLoader;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class PersistenceUnitInfoImpl implements PersistenceUnitInfo {
	
	private String persistenceUnitName;
	
	private String persistenceProviderClassName;
	
	private PersistenceUnitTransactionType transactionType;

	private DataSource jtaDataSource;
	
	private DataSource nonJtaDataSource;
	
	private URL persistenceUnitRootUrl;
	
	private Properties properties;
	
	private List<String> mappingFileNames = new ArrayList<String>();
	
	private List<URL> jarFileUrls = new ArrayList<URL>();
	
	private List<String> managedClassNames = new ArrayList<String>();
	
	private JpaClassLoader classLoader;
	
	public PersistenceUnitInfoImpl() {
	}
	
	public PersistenceUnitInfoImpl(
		Element element,
		Context context,
		URL persistenceUnitRootUrl) throws NamingException {
		persistenceUnitName = element.getAttribute("name");
		if (element.hasAttribute("transaction-type")) {
			transactionType = PersistenceUnitTransactionType.valueOf(element.getAttribute("transaction-type"));
		}
		NodeList puChilds = element.getChildNodes();
		for (int i = 0; i < puChilds.getLength(); i++) {
			String nodeName = puChilds.item(i).getNodeName();
			if ("jta-data-source".equals(nodeName)) {
				jtaDataSource = (DataSource) context.lookup(puChilds.item(i).getFirstChild().getNodeValue());
			} else if ("non-jta-data-source".equals(nodeName)) {
				nonJtaDataSource = (DataSource) context.lookup(puChilds.item(i).getFirstChild().getNodeValue());
			} else if ("provider".equals(nodeName)) {
				persistenceProviderClassName = puChilds.item(i).getFirstChild().getNodeName();
			} else if ("properties".equals(nodeName)) {
				properties = new Properties();
				Element ps = (Element) puChilds.item(i);
				NodeList props = ps.getElementsByTagName("property");
				for (int j = 0; j < props.getLength(); j++) {
					Element p = (Element) props.item(j);
					properties.setProperty(p.getAttribute("name"), p.getAttribute("value"));
				}
			}
		}
		this.persistenceUnitRootUrl = persistenceUnitRootUrl;
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		if (cl instanceof JpaClassLoader) {
			classLoader = (JpaClassLoader) cl;
		}
	}

	public String getPersistenceUnitName() {
		return persistenceUnitName;
	}

	public String getPersistenceProviderClassName() {
		return persistenceProviderClassName;
	}

	public PersistenceUnitTransactionType getTransactionType() {
		return transactionType;
	}

	public DataSource getJtaDataSource() {
		return jtaDataSource;
	}

	public DataSource getNonJtaDataSource() {
		return nonJtaDataSource;
	}

	public List<String> getMappingFileNames() {
		return mappingFileNames;
	}

	public List<URL> getJarFileUrls() {
		return jarFileUrls;
	}

	public URL getPersistenceUnitRootUrl() {
		return persistenceUnitRootUrl;
	}

	public List<String> getManagedClassNames() {
		return managedClassNames;
	}

	public boolean excludeUnlistedClasses() {
		// TODO 自動生成されたメソッド・スタブ
		return false;
	}

	public Properties getProperties() {
		return properties;
	}

	public ClassLoader getClassLoader() {
		if (classLoader instanceof ClassLoader) {
			return (ClassLoader) classLoader;
		}
		return null;
	}

	public void addTransformer(final ClassTransformer transformer) {
		classLoader.addTransformer(transformer);
	}

	public ClassLoader getNewTempClassLoader() {
		return classLoader.getNewTempClassLoader();
//		if (classLoader instanceof URLClassLoader) {
//			URLClassLoader cl = (URLClassLoader) classLoader;
//			return new URLClassLoader(cl.getURLs(), cl.getParent());
//		}
//		return Thread.currentThread().getContextClassLoader();
	}
	
}

Contextを渡す部分が気持ち悪いですが、実際には前回使ったJNDI登録用DataSourceを取得する為に利用しています。一次利用用クラスローダは、JpaClassLoaderから受け取ります。とりあえずDOMを使ってxmlを読み込んでますが、かなり適当に作ってるので後で作り直した方がよさそう・・・
さて、この構成で動かそうとしたのですが・・・なかなか動きませんでした。どうやら、一時利用ClassLoaderとEntityロード用ClassLoaderで、別々にJPAのクラス群をロードしていたのが原因みたいです。どうしようか迷ったのですが・・・・取り敢えず、%JAVA_HOME%\jre\lib\extフォルダに、HibernateEntityManagerに入っていたJPAのjarを入れてみることにしました(汗)・・・TopLinkもS2TigerもJPAAPIをJarの中に持っているので、分離できなかったんですよね・・・
以上の構成でなんとか動きました。TopLink EssentialsをHibernateと比較したときの最大のアドバンテージがこのN対1のLAZYロード機能だと思うので、これが利用できなければTopLinkを非APサーバ環境で使う意味はあまり無いのかなと思ってます。但し、この機能を使おうとすると、かなり個別の設定をしなければいけないので・・・お勧めの構成と言えるかどうかは微妙ですね。やはり、クラスエンハンス機能はHibernateのように既存のクラスロードとは切り離して設定してもらった方が使いやすいなと思います。ここら辺は、次のバージョンで簡略化してくれると嬉しいのですが・・・
とりあえず動作環境構築はここら辺まで。Tomcat上でのN:1 LAZYロード機能実現は、いろいろとめんどくさい構成になってしまいましたが・・・まぁ対応出来ただけよしとしたいです。後はプロジェクトにまとめなきゃ・・・というわけで、次からはMaven2の勉強に移ります。