Spring+S2JTA+Hibernate EntityManager

前回に引き続き、今度はTomcat上でSpring+Hibernateの連携を調べていた・・・のですが、どーも、Springのトランザクション機能が気に入らない・・・Seasar2によるいつでもどこでもJTAトランザクション環境に身も心も染まってしまった今、今更JDBCトランザクションとかやりたくないです。Springは自身が提供するTransactionインターフェイスによってその違いを吸収するって謳ってるけど、標準APIを独自フレームワークがラップして隠してしまうのは、やっぱり根本的におかしいと思うし、JPAHibernateは自身でJTAと連携するのでこういう隠蔽方法とは相性が悪いです。
というわけで、結構有名な方法だと思うのですが、S2JTAを利用してSpringのJtaTransactionManagerとHibernateEntityManagerをそれぞれ連携させ、JavaEE5サーバと同等環境を構築してみたいと思います。
まずは、pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>springexample</groupId>
	<artifactId>springexample</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>
	<name>springexample Maven Webapp</name>
	<url>http://maven.apache.org</url>
	<dependencies>
		<dependency>
			<groupId>struts</groupId>
			<artifactId>struts</artifactId>
			<version>1.2.9</version>
			<exclusions>
				<exclusion>
					<groupId>xml-apis</groupId>
					<artifactId>xml-apis</artifactId>
				</exclusion>
				<exclusion>
					<groupId>xalan</groupId>
					<artifactId>xalan</artifactId>
				</exclusion>
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>3.3.2.GA</version>
			<exclusions>
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.transaction</groupId>
					<artifactId>jta</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>javax.transaction</groupId>
			<artifactId>transaction-api</artifactId>
			<version>1.1</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc-struts</artifactId>
			<version>2.5.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
			<version>2.5.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
			<version>2.5.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>2.5.2</version>
		</dependency>
		<dependency>
			<artifactId>s2-extension</artifactId>
			<groupId>org.seasar.container</groupId>
			<version>2.4.23</version>
			<exclusions>
				<exclusion>
					<groupId>jboss</groupId>
					<artifactId>javassist</artifactId>
				</exclusion>
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.seasar.hibernate</groupId>
			<artifactId>s2hibernate-jpa</artifactId>
			<version>1.0.1</version>
			<exclusions>
				<exclusion>
					<groupId>org.seasar.container</groupId>
					<artifactId>s2-tiger</artifactId>
				</exclusion>
				<exclusion>
					<groupId>jboss</groupId>
					<artifactId>javassist</artifactId>
				</exclusion>
				<exclusion>
					<groupId>org.hibernate</groupId>
					<artifactId>hibernate</artifactId>
				</exclusion>
				<exclusion>
					<groupId>org.hibernate</groupId>
					<artifactId>hibernate-entitymanager</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.6</version>
		</dependency>
		<dependency>
			<groupId>jstl</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.15</version>
			<exclusions>
				<exclusion>
					<groupId>javax.mail</groupId>
					<artifactId>mail</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.jms</groupId>
					<artifactId>jms</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jdmk</groupId>
					<artifactId>jmxtools</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jmx</groupId>
					<artifactId>jmxri</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.4</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<finalName>springexample</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.5</source>
					<target>1.5</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-eclipse-plugin</artifactId>
				<configuration>
					<downloadSources>true</downloadSources>
					<downloadJavadocs>true</downloadJavadocs>
					<wtpversion>2.0</wtpversion>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<repositories>
		<repository>
			<id>java.net</id>
			<url>http://download.java.net/maven/1</url>
			<layout>legacy</layout>
		</repository>
		<repository>
			<id>jboss-maven2</id>
			<url>http://repository.jboss.org/maven2</url>
		</repository>
		<repository>
			<id>maven.seasar.org</id>
			<name>The Seasar Foundation Maven2 Repository</name>
			<url>http://maven.seasar.org/maven2</url>
		</repository>
	</repositories>
</project>

HibernateにS2のトランザクションマネージャーを認識させるためにS2Hibernate-JPAを導入しています。依存ライブラリは極力排除しているし、Springと被る依存ライブラリが多いので、実際にS2関連で追加されるjarは僅かです。
続いて、web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<display-name>springexamples</display-name>
	<context-param>
		<param-name>javax.servlet.jsp.jstl.fmt.locale</param-name>
		<param-value>ja_JP</param-value>
	</context-param>
	<context-param>
		<param-name>
			javax.servlet.jsp.jstl.fmt.fallbackLocale
		</param-name>
		<param-value>ja_JP</param-value>
	</context-param>
	<context-param>
		<param-name>
			javax.servlet.jsp.jstl.fmt.localizationContext
		</param-name>
		<param-value>messages</param-value>
	</context-param>
	<servlet>
		<servlet-name>s2servlet</servlet-name>
		<servlet-class>
			org.seasar.framework.container.servlet.S2ContainerServlet
		</servlet-class>
		<init-param>
			<param-name>configPath</param-name>
			<param-value>jdbc.dicon</param-value>
		</init-param>
		<init-param>
			<param-name>debug</param-name>
			<param-value>false</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>s2servlet</servlet-name>
		<url-pattern>/s2servlet</url-pattern>
	</servlet-mapping>

	<!-- Standard Action Servlet Configuration (with debugging) -->
	<servlet>
		<servlet-name>action</servlet-name>
		<servlet-class>
			org.apache.struts.action.ActionServlet
		</servlet-class>
		<init-param>
			<param-name>config</param-name>
			<param-value>/WEB-INF/struts-config.xml</param-value>
		</init-param>
		<init-param>
			<param-name>debug</param-name>
			<param-value>2</param-value>
		</init-param>
		<init-param>
			<param-name>detail</param-name>
			<param-value>2</param-value>
		</init-param>
		<load-on-startup>2</load-on-startup>
	</servlet>


	<!-- Standard Action Servlet Mapping -->
	<servlet-mapping>
		<servlet-name>action</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>


	<!-- The Usual Welcome File List -->
	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>
	<jsp-config>
		<jsp-property-group>
			<url-pattern>*.jsp</url-pattern>
			<page-encoding>UTF-8</page-encoding>
			<include-prelude>/WEB-INF/jsp/header.jsp</include-prelude>
			<trim-directive-whitespaces>
				true
			</trim-directive-whitespaces>
		</jsp-property-group>
	</jsp-config>
</web-app>

ActionServletの前にS2ContainerServletの初期化を行うように定義するのがポイントです。S2で作成したTransactionManagerをSpringがJNDI経由で受け取るからです。
続いてSeasar2の設定ファイル

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" 
	"http://www.seasar.org/dtd/components24.dtd">
<components namespace="jdbc">
    <include path="jta.dicon"/>
    
    <component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
    <component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory">
        <arg>
            <component class="org.seasar.extension.jdbc.impl.BasicStatementFactory"/>
        </arg>
        <property name="fetchSize">100</property>
        <!--
        <property name="maxRows">100</property>
        -->
    </component>

    <component name="xaDataSource"
            class="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource">
		<property name="serverName">"localhost"</property>
		<property name="databaseName">"springtest"</property>
		<property name="user">"root"</property>
		<property name="password">"root"</property>
    </component>
    
    <component name="connectionPool"
            class="org.seasar.extension.dbcp.impl.ConnectionPoolImpl">
        <property name="timeout">600</property>
        <property name="maxPoolSize">10</property>
        <property name="allowLocalTx">true</property>
        <destroyMethod name="close"/>
    </component>
    
    <component name="dataSource"
       class="org.seasar.extension.dbcp.impl.DataSourceImpl"/>
</components>

トランザクション、コネクションプール、DataSourceのみを管理する最小限の構成です。Springで直接定義しようとも思ったのですが、HibernateにTransactionManagerを渡す方法が解らなかったのでS2側で初期化することにしました。
続いてpersistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
	xmlns="http://java.sun.com/xml/ns/persistence"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
	<persistence-unit name="springexample" transaction-type="JTA">
		<provider>org.hibernate.ejb.HibernatePersistence</provider>
		<!-- 
		<jta-data-source>jdbc/dataSource</jta-data-source>
		 -->
		<properties>
			<property name="hibernate.dialect"
				value="org.hibernate.dialect.MySQL5InnoDBDialect" />
			<property name="hibernate.jndi.class"
				value="org.seasar.extension.j2ee.JndiContextFactory" />
			<property name="hibernate.transaction.manager_lookup_class"
				value="org.seasar.hibernate.jpa.transaction.SingletonTransactionManagerProxyLookup" />
			<property name="hibernate.show_sql" value="true" />
			<property name="hibernate.format_sql" value="true" />
			<property name="hibernate.use_sql_comments" value="false" />
		</properties>
	</persistence-unit>
</persistence>

データソースはSpringの方から渡してやるのでここでは定義しません。Hibernate.transaction.manager_lookup_classにS2Hibernateで提供されるLookupクラスを定義するのがポイントです。
(2008/03/29追記)transaction-type="JTA"の設定を明記しておかないと、SpringのPersistenceUnitInfoはRESOURCE_LOCALで定義してしまうみたいです。せっかくS2JTAを導入した意味が無くなってしまうので、ここは明記しておく必要があります。


そしてSpringの設定ファイル

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/tx
		http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
		http://www.springframework.org/schema/jee
		http://www.springframework.org/schema/jee/spring-jee-2.5.xsd">

	<jee:jndi-lookup id="s2TxManager"
		jndi-name="jta/TransactionManager">
		<jee:environment>
			java.naming.factory.initial=org.seasar.extension.j2ee.JndiContextFactory
		</jee:environment>
	</jee:jndi-lookup>

	<jee:jndi-lookup id="dataSource" jndi-name="jdbc/dataSource">
		<jee:environment>
			java.naming.factory.initial=org.seasar.extension.j2ee.JndiContextFactory
		</jee:environment>
	</jee:jndi-lookup>

	<bean id="transactionManager"
		class="org.springframework.transaction.jta.JtaTransactionManager">
		<constructor-arg ref="s2TxManager" />
	</bean>
	<tx:annotation-driven />

	<bean id="pum"
		class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
		<property name="defaultDataSource" ref="dataSource" />
	</bean>

	<bean id="emf"
		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
		<property name="persistenceUnitManager" ref="pum" />
	</bean>
	
	<bean
		class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

	<bean name="/login" class="springexample.action.LoginAction"
		scope="prototype"/>
</beans>

S2JTAで作成したTransactionManagerは、jee:jndi-lookupタグを利用して取得します。jee:environmentにS2用のJNDI定義を書くのがポイントです。
トランザクションマネージャはSpringのJtaTransactionManagerに渡し、DataSourceはDefaultPersistenceUnitManager経由でLocalContainerEntityManagerFactoryBeanに渡します。これで、JavaEEサーバ上で動かしたときと全く同じ感覚で、Spring上からHibernate EntityManagerを利用出来るようになりました。
JTA実装と割り切ってS2を使えば、Springとの組み合わせは意外に簡単で、しかもSpringのトランザクション定義が劇的に簡単になります。今回はTomcat上で動かしましたが、この構成なら当然ローカルアプリでも実現可能な筈です。ポイントは、Springの初期化前にS2を初期化するという一点のみ。
HibernateのようにJNDI経由でTransactionManagerを渡す必要が無い場合は、S2JTAの全てのクラスをSpring側で設定することも出来ます。でも、今回のようにS2からJNDI経由で取得する構成にしておけば、テスト時はこの構成を使い、JNDIの取得先を変更するだけですぐにアプリケーションサーバ環境に切り替えることが出来ます。
実はSpringユーザにこそ、S2JTAはお勧めなんじゃないか・・・と、今回試してみて感じました。みんなでJTAを使って楽にトランザクションを管理しましょう。