先日、こういうエントリを書きました。
Java SE環境で、JNDI × JTA(Narayana) × JPA - CLOVER
使ったのがNarayanaだったことと、「SE環境でもJTA使えるの?」みたいな反応をいただきましたので、もう少し書いてみることにしました。
今回は、OSSのJTA実装として単独で利用できる、以下の3つを使って先ほどのエントリと同様のことを行いたいと思います。
これらは、JTA 1.1の実装です。Java EE 7のJTAのバージョンは、1.2となっていますので、ご注意ください。
「先ほどのエントリと同様のこと」とは、以下の内容です。
- JPA+JTAによるトランザクション管理をJava SE環境で実行する
- トランザクションは、UserTransactionをJNDIルックアップして取得し、begin/commit/rollbackする
- JPAの実装は、Hibernateとする
- データベースは、H2を利用する
- (あんまり本質的でないこと:テストコードなので)トランザクションログは、極力カレントディレクトリに出力しないようにする
Entityのクラスについては、毎回定義するのも微妙なので、マルチプロジェクト構成にしてテストコードと設定ファイルだけ各JTA実装を利用する時に追加する流れにしたいと思います。
この条件のひとつ、「UserTransactionをJNDIルックアップする」があるがゆえに、ムダに複雑になったかも…?と思わなくも。
では、以下に順次書いていきます。
共通的な内容
まずは、ビルド設定。sbtのマルチプロジェクト構成を利用しますが、ここでは全体的な部分を書いて、ここの定義については追って記載していきます。
build.sbt
import sbt.Keys._ name := "jta-implementations-standalone-jpa" val projectScalaVersion = "2.11.8" scalaVersion := projectScalaVersion parallelExecution in Test := false lazy val commonSettings = Seq( version := "0.0.1-SNAPSHOT", organization := "org.littlewings", scalaVersion := projectScalaVersion, scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature"), updateOptions := updateOptions.value.withCachedResolution(true), parallelExecution in Test := false, fork in Test := true ) lazy val root = (project in file(".")) .aggregate(entity, bitronix, atomikos, jotm) lazy val entity = (project in file("entity")) .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq( // JPA "org.hibernate" % "hibernate-entitymanager" % "5.1.0.Final", // H2 Database "com.h2database" % "h2" % "1.4.191", // Test "org.scalatest" %% "scalatest" % "2.2.6" % "test" ) ) // 以降、サブプロジェクトの定義(後述)
JPAのEntity定義は、以下のものとします。
entity/src/main/scala/org/littlewings/javaee7/standalonejta/Book.scala
package org.littlewings.javaee7.standalonejta import javax.persistence._ import scala.beans.BeanProperty object Book { def apply(isbn: String, title: String, price: Int): Book = { val b = new Book b.isbn = isbn b.title = title b.price = price b } } @Entity @Table(name = "book") @SerialVersionUID(1L) class Book extends Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @BeanProperty var id: Long = _ @Column @BeanProperty var isbn: String = _ @Column @BeanProperty var title: String = _ @Column @BeanProperty var price: Int = _ }
また、テスト自体とテスト前のデータ削除の共通処理は全部まとめようと思ったので、ちょっとやりすぎ感はありますがテストコードの親クラスとして、以下のものを用意。
entity/src/test/scala/org/littlewings/javaee7/standalonejta/StandaloneJtaSpecSupport.scala
package org.littlewings.javaee7.standalonejta import javax.naming.InitialContext import javax.persistence.{NoResultException, Persistence} import javax.transaction.UserTransaction import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, FunSpec, Matchers} abstract class StandaloneJtaSpecSupport extends FunSpec with BeforeAndAfter with BeforeAndAfterAll with Matchers { before { val emf = Persistence.createEntityManagerFactory("javaee7.standalone.jta.pu") val em = emf.createEntityManager val userTransaction = new InitialContext().lookup("java:comp/UserTransaction").asInstanceOf[UserTransaction] userTransaction.begin() em.createNativeQuery("TRUNCATE TABLE book").executeUpdate() userTransaction.commit() em.close() emf.close() } describe(s"${getClass.getSimpleName.replace("JtaSpec", "")} JTA Spec") { it("simple JPA, commit") { val emf = Persistence.createEntityManagerFactory("javaee7.standalone.jta.pu") val em = emf.createEntityManager val userTransaction = new InitialContext().lookup("java:comp/UserTransaction").asInstanceOf[UserTransaction] userTransaction.begin() em.persist(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) em.persist(Book("978-4798042169", "わかりやすいJavaEEウェブシステム入門", 3456)) em.persist(Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION) ", 4536)) userTransaction.commit() val query = em .createQuery("SELECT b FROM Book b WHERE isbn = :isbn", classOf[Book]) .setParameter("isbn", "978-4798140926") query.getSingleResult.title should be("Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築") em.close() emf.close() } it("use simple JPA, rollback") { val emf = Persistence.createEntityManagerFactory("javaee7.standalone.jta.pu") val em = emf.createEntityManager val userTransaction = new InitialContext().lookup("java:comp/UserTransaction").asInstanceOf[UserTransaction] userTransaction.begin() em.persist(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) em.persist(Book("978-4798042169", "わかりやすいJavaEEウェブシステム入門", 3456)) em.persist(Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION) ", 4536)) userTransaction.rollback() val query = em .createQuery("SELECT b FROM Book b WHERE isbn = :isbn", classOf[Book]) .setParameter("isbn", "978-4798140926") val thrown = the[NoResultException] thrownBy query.getSingleResult thrown.getMessage should be("No entity found for query") em.close() emf.close() } } }
各JTAの実装を利用したプロジェクトでは、これらを利用してコードを書いていきます。テストコードについては、実質JTAやデータソースのセットアップを用意する感じになります。
では、書いていってみましょう。
Bitronix
最初は、割と名前をよく見るBitronixから。
GitHub - bitronix/btm: JTA Transaction Manager
プロジェクト定義に追加する内容は、以下のとおり。
lazy val bitronix = (project in file("bitronix")) .dependsOn(entity % "test->test") .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq( // Bitronix JTA "org.codehaus.btm" % "btm" % "2.1.4" ) )
Bitronixでは、JNDIの機能が組み込まれているので、別途JNDIサーバーは必要ありません。以下のようにjndi.propertiesを用意します。
bitronix/src/test/resources/jndi.properties
java.naming.factory.initial=bitronix.tm.jndi.BitronixInitialContextFactory
persistence.xmlは、以下のようになります。
bitronix/src/test/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1"> <persistence-unit name="javaee7.standalone.jta.pu" transaction-type="JTA"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <jta-data-source>jdbc/h2Ds</jta-data-source> <class>org.littlewings.javaee7.standalonejta.Book</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <!-- for Web Application Server, ex: Tomcat --> <property name="hibernate.transaction.jta.platform" value="org.hibernate.engine.transaction.jta.platform.internal.BitronixJtaPlatform"/> <property name="hibernate.jndi.class" value="bitronix.tm.jndi.BitronixInitialContextFactory"/> </properties> </persistence-unit> </persistence>
プロパティの最後の2つは今回はなくてもいいのですが、コンテナ上などで動かす時のことを考えると、あった方がよいみたいです。
Bitronix自体の設定も行います。
こちらを参考に、XAリソースやトランザクションログの設定。
Transaction manager configuration · bitronix/btm Wiki · GitHub
bitronix/src/test/resources/bitronix-default-config.properties
bitronix.tm.resource.configuration=./src/test/resources/resources.properties bitronix.tm.journal.disk.logPart1Filename=./target/btm1.tlog bitronix.tm.journal.disk.logPart2Filename=./target/btm2.tlog
リソースの設定ファイルは、実ファイルの位置で指定することになります。クラスパス上ではありません。トランザクションログは、targetディレクトリ配下に出力するようにしておきました。
続いて、リソースの設定。
Resource loader configuration · bitronix/btm Wiki · GitHub
bitronix/src/test/resources/resources.properties
resource.ds1.className=bitronix.tm.resource.jdbc.lrc.LrcXADataSource resource.ds1.uniqueName=jdbc/h2Ds resource.ds1.maxPoolSize=5 resource.ds1.driverProperties.driverClassName=org.h2.Driver resource.ds1.driverProperties.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 resource.ds1.allowLocalTransactions=true
uniqueNameで指定した値が、JNDI名になります。
テストコードは非常に簡単で、以下の内容だけ。
bitronix/src/test/scala/org/littlewings/javaee7/bitronix/BitronixJtaSpec.scala
package org.littlewings.javaee7.bitronix import bitronix.tm.TransactionManagerServices import org.littlewings.javaee7.standalonejta.StandaloneJtaSpecSupport class BitronixJtaSpec extends StandaloneJtaSpecSupport { // Init Bitronix override def beforeAll(): Unit = TransactionManagerServices.getResourceLoader.init() }
これだけで、UserTransactionもJNDIルックアップすることができます。が、何もしないとJPAの起動時にBitronixの内容が初期化されておらず、エラーになります。
使ってみて思ったことですが、とにかくエラーの内容がわかりにくいです。設定ファイルの必須項目が足りなかったりした場合に、データソースがJNDIルックアップしようとしてはじめてエラーとして発現します。しかも、なんでJNDIへ登録されていないかはわからないまま…デバッグしてみて、やっとわかりました。
JNDIの機能が組み込まれているところは、良いポイントだなと思います。とりあえず、Getting Started的に試してみるには簡単でよかったです。
Atomikos
続いて、Atomikos。
Atomikos - cloud-native transaction management for Java and REST - without application server
Getting Started with TransactionsEssentials | Documentation | Atomikos
プロジェクト定義に追加する内容は、以下のとおり。
lazy val atomikos = (project in file("atomikos")) .dependsOn(entity % "test->test") .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq( // Atomikos JTA "com.atomikos" % "transactions-jta" % "4.0.2", "com.atomikos" % "transactions-jdbc" % "4.0.2", "com.atomikos" % "transactions-hibernate4" % "4.0.2", // JNDI Server "jboss" % "jnpserver" % "4.2.2.GA" ) )
「transactions-jdbc」で、XADataSourceに関するクラスが追加され、「transactions-hibernate4」でHibernateのJtaPlatformに関するクラスが利用できるようになります。
AtomikosはJNDIの機能を持たないため、別途JNDI関連の依存関係が必要です。今回は、JBossのものを利用しました。
よって、JNDIの設定は以下のようになります。
atomikos/src/test/resources/jndi.properties
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces
persistence.xmlは、以下のとおり。
atomikos/src/test/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1"> <persistence-unit name="javaee7.standalone.jta.pu" transaction-type="JTA"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <jta-data-source>jdbc/h2Ds</jta-data-source> <class>org.littlewings.javaee7.standalonejta.Book</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.transaction.jta.platform" value="com.atomikos.icatch.jta.hibernate4.AtomikosPlatform"/> </properties> </persistence-unit> </persistence>
「hibernate.transaction.jta.platform」で、Atomikos用のクラスを設定することで、AtomikosとHibernateを連携させることができます。「transactions-hibernate4」には、これともう1種類のJtaPlatformに関するクラスしか入っていません。
Hibernate 4用っぽいですが、一応大丈夫みたいです…。
Atomikosの設定としては、テスト用なのでトランザクションログをオフにしました。
atomikos/src/test/resources/jta.properties
# for unit test com.atomikos.icatch.enable_logging=false
jta.propertiesの設定は、こちらを参照のこと。
JTA Properties | Documentation | Atomikos
データソースの設定などは、テストコードで行います。
atomikos/src/test/scala/org/littlewings/javaee7/atomikos/AtomikosJtaSpec.scala
package org.littlewings.javaee7.atomikos import javax.naming.InitialContext import com.atomikos.icatch.config.UserTransactionServiceImp import com.atomikos.icatch.jta.{TransactionManagerImp, UserTransactionImp} import com.atomikos.jdbc.AtomikosDataSourceBean import org.h2.jdbcx.JdbcDataSource import org.jnp.interfaces.NamingParser import org.jnp.server.NamingBeanImpl import org.littlewings.javaee7.standalonejta.StandaloneJtaSpecSupport class AtomikosJtaSpec extends StandaloneJtaSpecSupport { val namingBean: NamingBeanImpl = new NamingBeanImpl override def beforeAll(): Unit = { namingBean.start() val context = new InitialContext new UserTransactionServiceImp().init() val transactionManager = TransactionManagerImp.getTransactionManager val userTransaction = new UserTransactionImp context.bind("java:/TransactionManager", transactionManager) context.bind("java:comp/UserTransaction", userTransaction) namingBean.getNamingInstance.createSubcontext(new NamingParser().parse("jdbc")) val atomikosDataSourceBean = new AtomikosDataSourceBean atomikosDataSourceBean.setUniqueResourceName("jdbc/h2Ds") atomikosDataSourceBean.setXaDataSourceClassName(classOf[JdbcDataSource].getName) val properties = new java.util.Properties properties.put("URL", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1") atomikosDataSourceBean.setXaProperties(properties) context.bind("java:/jdbc/h2Ds", atomikosDataSourceBean) } override def afterAll(): Unit = { namingBean.stop() } }
JNDIのセットアップは、Narayanaの時と同じです。
JNDIリソースとして、TransactionManager、UserTransactionを登録。データソースについては、AtomikosDataSourceBeanで設定します。
JOTM
最後は、JOTM。
http://jotm.ow2.org/xwiki/bin/view/Main/
個人的には、スタンドアロンなJTAの実装としてはこちらのイメージがありました。個人的には、ですが。
プロジェクト定義に追加する内容は、以下のとおり。
lazy val jotm = (project in file("jotm")) .dependsOn(entity % "test->test") .settings(commonSettings: _*) .settings( resolvers += "ow2 repository" at "http://repository.ow2.org/nexus/content/repositories/ow2-legacy", libraryDependencies ++= Seq( // JOTM JTA "org.ow2.jotm" % "jotm-core" % "2.2.3", "org.ow2.spec.ee" % "ow2-connector-1.5-spec" % "1.0.8" % "runtime", // XA DataSource "com.experlog" % "xapool" % "1.5.0" ) )
JOTMの場合は、「jotm-core」を引き込むとJNDI関連の依存関係も引き込まれるので、こちらを利用します。「xapool」は、XAデータソースを定義するのに利用します。
jotm/src/test/resources/jndi.properties
java.naming.factory.initial=org.ow2.carol.jndi.spi.MultiOrbInitialContextFactory
persistence.xmlの設定。
jotm/src/test/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1"> <persistence-unit name="javaee7.standalone.jta.pu" transaction-type="JTA"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <jta-data-source>java:/h2Ds</jta-data-source> <class>org.littlewings.javaee7.standalonejta.Book</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.transaction.jta.platform" value="org.hibernate.engine.transaction.jta.platform.internal.JOTMJtaPlatform"/> <property name="hibernate.jndi.class" value="org.ow2.carol.jndi.spi.MultiOrbInitialContextFactory"/> </properties> </persistence-unit> </persistence>
JtaPlatformやJNDIファクトリクラスの指定がありますが、他のJTAの実装でpersistence.xmlで使った時と、実はJNDI名が異なります。
<jta-data-source>java:/h2Ds</jta-data-source>
ここで利用したJNDI実装で、サブコンテキストが作成できなかったからです…。
jotm/src/test/scala/org/littlewings/javaee7/jotm/JotmJtaSpec.scala
package org.littlewings.javaee7.jotm import java.rmi.registry.LocateRegistry import javax.naming.InitialContext import org.enhydra.jdbc.standard.StandardXADataSource import org.h2.Driver import org.littlewings.javaee7.standalonejta.StandaloneJtaSpecSupport import org.objectweb.jotm.Jotm class JotmJtaSpec extends StandaloneJtaSpecSupport { val jotm: Jotm = new Jotm(true, false) override def beforeAll(): Unit = { LocateRegistry.createRegistry(1099) val dataSource = new StandardXADataSource dataSource.setTransactionManager(jotm.getTransactionManager) dataSource.setDriverName(classOf[Driver].getName) dataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1") val context = new InitialContext context.bind("java:/TransactionManager", jotm.getTransactionManager) context.bind("java:comp/UserTransaction", jotm.getUserTransaction) context.bind("java:/h2Ds", dataSource) } override def afterAll(): Unit = { jotm.stop() } }
JOTMでは、JotmクラスのインスタンスからTransactionManagerやUserTransactionが取得できます。が、JNDI登録するには、RMIを利用することになります…。しかも、サブコンテキストの作成がサポートされていないみたいなので、前述のとおり諦めました、と。
XAデータソースは、StandardXADataSourceを利用してTransactionManagerを設定のうえ、接続定義を行います。
最後に、Jotm#stopを呼び出して終了です。
まとめ
Java SE環境で、各種JTAの実装+JPAを動かすサンプルを書いてみました。
ホントにJTA(XAトランザクション)が必要かどうかはさておき…アプリケーションサーバーが提供している実装以外にも、いくつかJTA実装があってSE環境でも使えることは知っておいてもいいのではないかなと思います。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/jta-implementations-standalone-jpa
参考)
Infinispanのドキュメントにほぼすべてパターンが載っていて、とても助かりました。
27.2. Implementing standalone JPA JTA Hibernate application outside J2EE server using Infinispan 2nd level cache
Hibernateと連動させたい場合は、HibernateのJtaPlatformに用意されているものを見ておくとよいかもしれません。
https://github.com/hibernate/hibernate-orm/tree/5.1.0/hibernate-core/src/main/java/org/hibernate/engine/transaction/jta/platform/internal