CLOVER🍀

That was when it all began.

Java SE環境で、JNDI × JTA(Bitronix/Atomikos/JOTM) × JPA

先日、こういうエントリを書きました。

Java SE環境で、JNDI × JTA(Narayana) × JPA - CLOVER

使ったのがNarayanaだったことと、「SE環境でもJTA使えるの?」みたいな反応をいただきましたので、もう少し書いてみることにしました。

今回は、OSSのJTA実装として単独で利用できる、以下の3つを使って先ほどのエントリと同様のことを行いたいと思います。

これらは、JTA 1.1の実装です。Java EE 7のJTAのバージョンは、1.2となっていますので、ご注意ください。

「先ほどのエントリと同様のこと」とは、以下の内容です。

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