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 から登録可能な名前を指定すればいいだけなので、あまり意味は無いと思われる。