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もJPAのAPIをJarの中に持っているので、分離できなかったんですよね・・・
以上の構成でなんとか動きました。TopLink EssentialsをHibernateと比較したときの最大のアドバンテージがこのN対1のLAZYロード機能だと思うので、これが利用できなければTopLinkを非APサーバ環境で使う意味はあまり無いのかなと思ってます。但し、この機能を使おうとすると、かなり個別の設定をしなければいけないので・・・お勧めの構成と言えるかどうかは微妙ですね。やはり、クラスエンハンス機能はHibernateのように既存のクラスロードとは切り離して設定してもらった方が使いやすいなと思います。ここら辺は、次のバージョンで簡略化してくれると嬉しいのですが・・・
とりあえず動作環境構築はここら辺まで。Tomcat上でのN:1 LAZYロード機能実現は、いろいろとめんどくさい構成になってしまいましたが・・・まぁ対応出来ただけよしとしたいです。後はプロジェクトにまとめなきゃ・・・というわけで、次からはMaven2の勉強に移ります。