TomcatでJNDIのcomp/コンテキストに値を登録する
のようなWebアプリケーションをTomcat6 の上で動かしたい。折角なので JNDI を使って、データベースやトランザクションマネージャの設定は全て Tomcat 側で行いたいんだけど、Atomikosのサイトにある設定 では Spring から TransactionManager を見つけられないらしく、「Springが想定しているJNDI名でTransactionManagerか見つかりません」系の警告が山ほど出る。
で、Spring が想定している comp/TransactionManager などの名前で TransactionManager を登録しようにも、Tomcat では comp/ のコンテキストには値をセットする手段が(Transaction以外には)無いようで。
ということで、無理やり comp/TransactionManager とかに TransactionManager を登録するためのリスナ。こんなんでいいのかは不明。
package example.tomcat6; import javax.naming.Context; import javax.naming.NameAlreadyBoundException; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.StringRefAddr; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.apache.catalina.core.NamingContextListener; import org.apache.catalina.core.StandardContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.naming.factory.Constants; import org.apache.naming.ContextAccessController; import org.apache.naming.ResourceRef; import org.apache.naming.factory.BeanFactory; /** * A LifeCycleListener which binds the JTA transaction manager to * "java:comp/TransactionManager" on the AFTER_START event of * {@link org.apache.catalina.core.StarndardContext}. * * example: <pre>{@literal * <Context path="/webapp" ...> * <Listener className="example.tomcat6.TransactionManagerBindingListener" * transactionManager="com.atomikos.icatch.jta.UserTransactionManager" * factory="com.atomikos.icatch.jta.TransactionManagerFactory" /> * </Context> * }</pre> */ public final class TransactionManagerBindingListener implements LifecycleListener { private static Log log = LogFactory .getLog(TransactionManagerBindingListener.class); private static final String TX_MANAGER_NAME = "TransactionManager"; private String transactionManager; private String factory; private static interface ContextCallback { void exec(Context context); } public void lifecycleEvent(LifecycleEvent event) { if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) { afterStartEvent(event.getLifecycle()); } else { if (log.isDebugEnabled()) { log.debug(String.format( "Ignoring LifecycleEvent [%s] fired by [%s]", event.getType(), event.getLifecycle())); } } } /** * Sets the JTA transaction manager class name. * * @param transactionManager */ public void setTransactionManager(String transactionManager) { this.transactionManager = transactionManager; } /** * Sets the JNDI ObjectFactory class name. * * If {@code factory} is not specified, {@link BeanFactory} is used. * * @param factory * @see javax.naming.spi.ObjectFactory */ public void setFactory(String factory) { this.factory = factory; } private void afterStartEvent(final Object lifecycle) { checkPropertiesSet(); ContextCallback callback = new ContextCallback() { public void exec(Context compContext) { String factoryClassName = (factory == null) ? BeanFactory.class.getName() : factory; // equivalent to: // <Resource auth="Container" // type="{transactionManagerTypeName}" factory="{factoryClassName}" /> Reference txManagerRef = new ResourceRef(transactionManager, null, null, "Container"); txManagerRef.add( new StringRefAddr(Constants.FACTORY, factoryClassName)); try { compContext.bind(TX_MANAGER_NAME, txManagerRef); log.info(String.format( "bound TransactionManager [%s] in this context [%s]", transactionManager, lifecycle)); } catch (NameAlreadyBoundException e) { log.warn(String.format( "java:comp/%s is already bound in this context [%s]", TX_MANAGER_NAME, lifecycle)); } catch (NamingException e) { throw new RuntimeException(String.format( "Error while binding TransactionManager in this context [%s]", lifecycle), e); } } }; doInCompContext(lifecycle, callback); } // TODO: Is there any other solution? private void doInCompContext(Object lifecycle, ContextCallback callback) { if (!(lifecycle instanceof StandardContext)) { log.warn(String.format( "Lifecycle class is not a StandardContext: [%s]", lifecycle .getClass())); return; } StandardContext context = (StandardContext) lifecycle; NamingContextListener namingContextListener = context .getNamingContextListener(); if (namingContextListener == null) { log.warn(String.format( "No NamingContextListener in the StandardContext [%s]", context .getName())); return; } String contextName = namingContextListener.getName(); ContextAccessController.setWritable(contextName, context); try { callback.exec(namingContextListener.getCompContext()); } finally { ContextAccessController.setReadOnly(contextName); } } private void checkPropertiesSet() { if (transactionManager == null) { throw new IllegalArgumentException( "'transactionManagerClassName' is not set."); } } }
そもそもSpring側で comp/env/TransactionManager とかの Tomcat から登録可能な名前を指定すればいいだけなので、あまり意味は無いと思われる。