TopLink EssentialsをTomcatで動かす その4

TopLink Essentials V2 build10が出てました。EntityManager作成時にjoinTransactionをしてないのでトランザクション終了時にエラーが発生する問題が解決されてました。PersistenceProviderが違うときに落ちる問題はまだ直ってないっぽい。
さて、前々回から色々やってた、Tomcat上でN:1LAZYロードを有効にする為の対応ですが、とりあえず解決策が見つかりました。
まずはClassLoaderの問題。TempClassLoaderはつまりClassエンハンス対象のEntityのみを独立してロード出来ればいいので、そこに絞って考えてみました。PersistenceUnitInfoはルートURLとJARのURLを保持しているので、この情報を使ってURLClassLoaderを作成すればいけるかも?
というわけで、早速作ってみました。

	public class TempClassLoader extends URLClassLoader {
		
		public TempClassLoader(URL[] urls) {
			super(urls);
		}

		public TempClassLoader(URL[] urls, ClassLoader parent) {
			super(urls, parent);
		}

		public TempClassLoader(URL[] urls, ClassLoader parent,
				URLStreamHandlerFactory factory) {
			super(urls, parent, factory);
		}

		@Override
		protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
			if (!name.startsWith("java")) {
				Class<?> c = findLoadedClass(name);
				if (c == null) {
					try {
						c = findClass(name);
					} catch (ClassNotFoundException e) {
						return super.loadClass(name, resolve);
					}
				}
				if (resolve) {
					resolveClass(c);
				}
				return c;
			} else {
				return super.loadClass(name, resolve);
			}
		}

	}

このクラスローダを、PersistenceUnitInfo.getNewTempClassLoaderメソッドで作成

	public ClassLoader getClassLoader() {
		return Thread.currentThread().getContextClassLoader();
	}

	public ClassLoader getNewTempClassLoader() {
		ClassLoader cl = getClassLoader();
		List<URL> urlList = new ArrayList<URL>();
		if ("file".equals(getPersistenceUnitRootUrl().getProtocol())) {
			String root = getPersistenceUnitRootUrl().toString() + "/";
			try {
				urlList.add(new URL(root));
			} catch (MalformedURLException e) {
				throw new RuntimeException(e);
			}
		} else {
			urlList.add(getPersistenceUnitRootUrl());
		}
		urlList.addAll(getJarFileUrls());
		return new TempClassLoader(urlList.toArray(new URL[urlList.size()]), cl);
	}

JARを使ったときはまだ試してませんが、FILE上にpersistence.xmlがあるときはこれで動きました。これで、JPAのライブラリをシステムクラスローダ上に置く必要は無くなりました。
そしてClassTransformerを受け取ったときの対応ですが・・・ClassLoaderを拡張して対応するのはやめて、S2のWeaving機能を参考にしてリフレクションを利用することにしました。これなら、ContextClassLoader上に直接ロードすることが出来るんですね。なるほど・・・
しかし一点問題があります。ClassTransformerインターフェイスには、エンハンスするクラス情報を取得するメソッドが存在しません。クラスファイルのバイト配列を取得し、transformメソッドを実行してみないと、エンハンス対象かどうかがわかりません。AutoRegisterなどを使って、自動登録対象のクラスパス上にあるクラスファイルを総当りでなめるって手もありますが・・・作成したクラスが増えると初期化時に待たされて問題になりそうですし・・・
取り敢えずHibernateを使うときはClassTransformerを利用することはほぼ無いだろうということで、ここはTopLink依存で作成することにしました。TopLinkのClassTransformer実装クラスには、エンハンス対象のクラス名をMapで取得できるメソッドがあります。これを使って、Transformerを渡されたときに、一気にClassを登録することにしました。

public class TopLinkPersistenceUnitInfoImpl extends PersistenceUnitInfoImpl {

	public TopLinkPersistenceUnitInfoImpl() {
	}

	public TopLinkPersistenceUnitInfoImpl(Element element, Context context,
			URL persistenceUnitRootUrl, ResourceAutoDetector detector) {
		super(element, context, persistenceUnitRootUrl, detector);
	}

	@Override
	public void addTransformer(final ClassTransformer transformer) {
		if (transformer instanceof TopLinkWeaver) {
			TopLinkWeaver weaver = (TopLinkWeaver) transformer;
			ClassLoader cl = getClassLoader();
			for (Object obj : weaver.getClassDetailsMap().keySet()) {
				String className = (String) obj;
				try {
					byte[] temp = getClassBytes(className);
					byte[] bytes = transformer.transform(cl, className, null, protectionDomain, temp);
					if (bytes != null) {
						defineClassMethod.invoke(
								cl,
								className.replace('/', '.'),
								bytes,
								0,
							    bytes.length,
							    protectionDomain);
					}
				} catch (IllegalArgumentException e) {
					throw new RuntimeException(e);
				} catch (IllegalAccessException e) {
					throw new RuntimeException(e);
				} catch (InvocationTargetException e) {
					throw new RuntimeException(e);
				} catch (IllegalClassFormatException e) {
					throw new RuntimeException(e);
				} catch (IOException e) {
					throw new RuntimeException(e);
				}

			}
		}
		
	}

}

TopLink依存の処理のみ子クラスに切り出す形にしてみました。バイト配列は取り敢えずClassLoaderから取得したInputStreamを変換してます(JavassistのCtClassが使えるかなと思ったのですが、上手くいきませんでした・・・)。この処理によって、独自のClassLoaderを用意する必要が無くなり、Tomcat依存のクラスを作る必要も無くなりました。
残る問題は、EntityManagerFactory作成時に対象クラスが既にロードされていた場合、クラス登録に失敗すること。以前はS2ContainerServletを継承した独自の初期化サーブレットを作成していたのですが、できればこういうクラスは無くしたいですね。どうしようか悩んだのですが・・・EntityManagerFactoryのコンポーネントをs2container.diconに登録することにしました。作成したEntityManagerFactoryをstaticなMap上に保存するようにすれば、S2の初期化中に作成したEntityManagerFactoryをそのまま使うことが出来ますし。
以上の構成で動きました。前回までと比べると、かなり設定が簡単になって、汎用性も上がったのではないかと思います。ついでに、PersistenceUnitInfo作成時にResourceAutoDetectorを使ってXMLファイルを自動登録する処理も追加してみました。今はreference用のClassを設定ファイルに書いてますが、SVNを見るとインターフェイスの定義が変わってるみたいなので、しばらくしたらこちらの方も作り直そうと思います。ルートのURLとpersistence.xmlに登録したJAR上のXMLファイルを自動登録するように作れば、Entityクラスと同じ感覚で自動登録されるのでわかりやすそうです。S2DaoのようにDaoのメソッドと合わせた形でファイル名を決めていく規約にすると更にわかりやすいかも。
とりあえずはここまでかな? Maven2も基礎の基礎だけ勉強したので、ひとまずアップできる体裁だけは整いました。
あと、javaagentは色々やってみましたが、やっぱりちょっと使いづらい部分があるので、TopLinkの場合はPersistenceUnitInfoを使って初期化する方法を標準とした方がよいのかも。まだ試していませんが、一応Hibernateも同じ仕組みで動くようにしておきたいなと思います。