環境
OS
Windows 7
AP サーバー
GlassFish 4.1 Open Source Edition (一部 Wildfly 9.0.1 使用)
Java
1.8.0_60
DB サーバー
MySQL 5.5.28, for Win64 (x86)
JTA とは
Java Transaction API の略。
Java で トランザクションマネージャ を扱うための各種 API を定義した仕様。
トランザクションマネージャとは、分散トランザクションの管理を行うサービス(ミドルウェア)で、 Java EE サーバーだと EJB コンテナがその役割を担っている。
DTP
Distributed Transaction Processing の略(直訳するなら、分散トランザクション処理?)。
X/Open という組織が策定した、分散トランザクションについての標準規格。JTA が扱う分散トランザクションも、この DTP に準拠している。
X/Open は UNIX に関する技術の標準化を行う団体だったが、 1996 年に Open Software Foundation と合併して、現在は The Open Group という組織になっている。
DTP で登場するソフトウェア
DTP では、分散トランザクションに登場するソフトウェアとして、以下のものを挙げている。
リソースマネージャ
トランザクション機能を備えたリソースを管理するソフトウェア。
リレーショナルデータベース製品や、メッセージ指向ミドルウェアなどがこれにあたる。
トランザクションマネージャ
複数のリソースマネージャを管理し、分散トランザクションを制御するソフトウェア。
アプリケーション
トランザクションを利用するソフトウェア。
2フェーズコミット
分散トランザクションを実現するための具体的なプロトコル。
トランザクションのコミットを2回のフェーズに分けて行う。
第1フェーズでは、各リソースマネージャに現在のトランザクションがコミット可能かどうかを確認する。
全てのリソースマネージャがコミット可能であると返事をしたら、第2フェーズとして各リソースマネージャにコミットの指示が飛ぶ。
どれか1つでもコミット不可の返事をしたリソースマネージャが存在した場合は、全てのリソースマネージャにロールバックの指示が飛ぶ。
こうすることで、分散トランザクションでの原子性の担保が実現されている。
より細かい話は 分散トランザクションに挑戦しよう! を参照のこと。
DTP では、このプロトコルを実現するための具体的なインターフェースが定義されている。
インターフェース
DTP では、大きく以下の2つのインターフェースが定義されている。
TX インターフェース
トランザクションマネージャが、アプリケーションに対して公開するインターフェース。
XA インターフェース
リソースマネージャが、トランザクションマネージャに対して公開するインターフェース。
JTA が定義するインターフェース
JTA では、以下の3つのインターフェースが定義されている。
javax.transaction.TransactionManager
トランザクションマネージャが、 アプリケーション・サーバー に対して公開するインターフェース。
EJB がサポートしているコンテナ管理のトランザクション(CMT)は、この TransactionManager
を使って実現されている。
javax.transaction.UserTransaction
トランザクションマネージャが、 アプリケーション に対して公開するインターフェース。
トランザクションの開始・終了をコンテナに任せないとき(BMT)は、この UserTransaction
を EJB などにインジェクションして明示的にトランザクション境界を定義する。
javax.transaction.xa.XAResource
リソースマネージャが、トランザクションマネージャに対して公開するインターフェース。
トランザクションマネージャは、この XAResource
に定義されたメソッドを利用して、 DTP で定義された分散トランザクションを実現している。
XAResource
の実装は、各リソースマネージャが用意する。
例えば、 MySQL の場合は JDBC ドライバの中に XAResource
を実装したクラスが含まれている(com.mysql.jdbc.jdbc2.optional.MysqlXAConnection
)。
下図は、JTA の仕様書に描かれている図で、各インターフェースとソフトウェアの関連を把握するのに丁度いい図になっている。
グローバルトランザクションとローカルトランザクション
JTA の設定のときなどに目にする、これらの言葉について。
グローバルトランザクション
分散トランザクションのこと。
トランザクションの対象となるリソースマネージャが複数存在するときに利用する。
ローカルトランザクション
リソースマネージャが提供するトランザクションの API をそのまま利用すること。
JDBC なら、 java.sql.Connection
の commit()
メソッドとかを直接利用することになる。
トランザクションの対象となるリソースマネージャが1つしか存在しないときに利用する。
GlassFish で分散トランザクションを使用する
JTA を使うということは、すなわち分散トランザクションを使うことになるので、「明示的に分散トランザクションを使用する」といった設定は必要ない。
強いて言うなら、データソースの登録で XA
を実装したクラスを指定しないといけない、という点くらい。
MySQL を使って XA
に対応した2つのデータソースを登録する。
JDBC ドライバはあらかじめインストールしておくこと。
データソースの登録
コネクションプールの登録
- 管理コンソールを開き、 [Resources] > [JDBC] > [JDBC Connection Pools] を選択する。
- [New...] をクリック。
- 以下入力。
- [Pool Name] に適当な名前を入れる。
- [Resource Type] で
javax.sql.XADataSource
を選択。 - [Database Driver Vendor] で
MySql
を選択。
- [Next] をクリックして、次のページで以下のプロパティを設定。
-
Username
,Password
,ServerName
,DatabaseName
-
- 同じ要領で、名前違いのデータソースをもう1つ登録する。
JDBC リソースの登録
- 管理コンソールから、 [Resources] > [JDBC] > [JDBC Resources] を選択する。
- [New...] をクリック。
- 以下入力。
- [JNDI Name] に適当な名前を入れる。
- [Pool Name] で、先ほど登録したコネクションプールを選択。
- [OK] をクリック。
データベース
こんなテーブルを用意しておく。
実装+動作確認
package sample.jta.ejb;
import java.sql.Connection;
import java.sql.PreparedStatement;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
public class HelloEjb {
@Resource(lookup = "jdbc/mysql_global_1") // ★データソースを注入
private DataSource ds1;
@Resource(lookup = "jdbc/mysql_global_2") // ★データソースを注入
private DataSource ds2;
@Resource
private TransactionSynchronizationRegistry tx;
public void execute() {
this.insert(this.ds1, "hoge");
this.insert(this.ds2, "fuga");
System.out.println("txKey = " + tx.getTransactionKey());
}
private void insert(DataSource ds, String value) {
// ★ TEST_TABLE にレコードを INSERT
try (Connection con = ds.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO TEST_TABLE (VALUE) VALUES (?)");
) {
ps.setString(1, value);
ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
}
}
HelloEjb#execute()
を実行するように外部からアクセスする。
情報: txKey = JavaEETransactionImpl: txId=21 nonXAResource=null jtsTx=com.sun.jts.jta.TransactionImpl@c3161dfc ...]
実行後のテーブル
複数のデータソースを、1つの EJB (トランザクション)の中で利用できている。
非 XA なデータソースを登録するとどうなる?
GlassFish に限らず、 AP サーバーのデータソースには、 XA を実装していないデータソースも登録できる。
JTA は分散トランザクションのための API なので、 XA に対応していないデータソースは利用できなさそうだが、実際は登録できるし、そのまま JTA 経由で利用もできてしまう。
何が起こっているのか調べてみる。
データソースを登録する
先ほどと同じ要領で、非 XA なデータソースを登録する。
実装
package sample.jta.ejb;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
public class NonXADataSourceEjb {
@Resource(lookup = "jdbc/mysql_local_1")
private DataSource ds;
@Resource
private TransactionSynchronizationRegistry tx;
public void execute() throws Exception {
try (Connection con = ds.getConnection()) { // ★ ds を JTA に参加させる
Object key = tx.getTransactionKey();
System.out.println("txKey = " + key);
// ★ TransactionKey が持つ nonXAResource の具体的なクラス名を確認する
Object nonXAResource = this.getFieldValue(key, "nonXAResource");
System.out.println("nonXAResource.class = " + nonXAResource.getClass());
}
}
public Object getFieldValue(Object obj, String fieldName) throws Exception {
if (obj == null) return null;
Class<?> clazz = obj.getClass();
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
Object result = field.get(obj);
field.setAccessible(false);
return result;
} catch (NoSuchFieldException e) {
return null;
}
}
}
非 XA なデータソースを JTA 経由で利用すると、 nonXAResource
といういかにもそれっぽいインスタンスが生成されているようなので、その値をリフレクションを使って無理やり取得している。
情報: txKey = JavaEETransactionImpl: txId=39 nonXAResource=715 jtsTx=null ...]
情報: nonXAResource.class = class com.sun.enterprise.resource.ResourceHandle
nonXAResource
の実体は com.sun.enterprise.resource.ResourceHandle
というクラスらしい。
このクラスのコードは以下になる。
GC: ResourceHandle - com.sun.enterprise.resource.ResourceHandle (.java) - GrepCode Class Source
public class ResourceHandle implements
com.sun.appserv.connectors.internal.api.ResourceHandle, TransactionalResource {
...
private XAResource xares;
...
よく見ると、中に XAResource
が入っている。
このフィールドの実体を調べてみると、 com.sun.enterprise.resource.ConnectorXAResource
というクラスだった。
コードは以下。
このクラスの、 commit()
メソッドを覗いてみる。
public void commit(Xid xid, boolean onePhase) throws XAException {
try {
ResourceHandle handle = getResourceHandle();
ManagedConnection mc = (ManagedConnection) handle.getResource();
mc.getLocalTransaction().commit(); // ★ getLocalTransaction() してる!
} catch (Exception ex) {
handleResourceException(ex);
}finally{
resetAssociation();
}
}
ConnectorXAResource
は XAResource
を実装しているものの、実はローカルトランザクションを使っているっぽい。
つまり、データソースが XA に対応していなかったとしても、内部でラップして XAResource
に擬似的に対応させることで、 JTA 経由で利用できるようになっているのだろう。
一応 Wildfly でも同じように確認したが、やはり非 XA データソースは、内部でローカルトランザクションを使うクラスにラップされて利用されていた。
複数のローカルトランザクションを使うとどうなる?
package sample.jta.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
@Stateless
public class MultiNonXADataSourceEjb {
@Resource(lookup = "jdbc/mysql_local_1")
private DataSource ds1;
@Resource(lookup = "jdbc/mysql_local_2")
private DataSource ds2;
public void execute() throws Exception {
try (Connection c1 = ds1.getConnection();
Connection c2 = ds2.getConnection()) {}
}
}
情報: ejb = MultiNonXADataSourceEjb, method = execute
重大: RAR5029:Unexpected exception while registering component
java.lang.IllegalStateException: Local transaction already has 1 non-XA Resource: cannot add more resources.
at com.sun.enterprise.transaction.JavaEETransactionManagerSimplified.enlistResource(JavaEETransactionManagerSimplified.java:345)
...
警告: RAR7132: Unable to enlist the resource in transaction. Returned resource to pool. Pool name: [ mysql_local_2 ]
警告: RAR5117 : Failed to obtain/create connection from connection pool [ mysql_local_2 ]. Reason : com.sun.appserv.connectors.internal.api.PoolingException: java.lang.IllegalStateException: Local transaction already has 1 non-XA Resource: cannot add more resources.
警告: RAR5114 : Error allocating connection : [Error in allocating a connection. Cause: java.lang.IllegalStateException: Local transaction already has 1 non-XA Resource: cannot add more resources. ]
重大: java.lang.reflect.InvocationTargetException
...
ds2
を JTA に参加させようとした時点で例外がスローされた。
Local transaction already has 1 non-XA Resource: cannot add more resources.
とあるので、非 XA なデータソースを複数使うことはできないようになっている。
データベースコネクションが JTA に参加するタイミング
説明を省いていたが、データベースコネクションが JTA の管理下に置かれるタイミングは、上記の例からも分かる通り DataSource#getConnection()
を実行したタイミングになる。
ただし、 JTA の仕様的にそうなのか、 GlassFish の実装的にそうなのかは分からない。
アノテーション
JTA1.2 (Java EE 7) から追加されたアノテーションに、 @Transactional
と @TransactionScoped
がある。
これらのアノテーションを利用することで、 CDI 管理ビーンでコンテナ管理のトランザクションが利用できるようになる。
@Transactional
このアノテーションで CDI 管理ビーンをアノテートすることで、 EJB と同じように宣言的なトランザクションが使えるようになる。
基本
package sample.jta.ejb;
import java.sql.Connection;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.sql.DataSource;
import javax.transaction.TransactionSynchronizationRegistry;
import sample.jta.cdi.TransactionalBean;
@Stateless
public class TransactionalEjb {
@Resource(lookup = "jdbc/mysql_local_1")
private DataSource ds;
@Resource
private TransactionSynchronizationRegistry tx;
@Inject
private TransactionalBean bean;
public void execute() throws Exception {
try (Connection con = ds.getConnection()) {
System.out.println("TransactionalEjb : txKey=" + tx.getTransactionKey());
this.bean.requreisNew();
this.bean.required();
}
}
}
package sample.jta.cdi;
import java.sql.Connection;
import javax.annotation.Resource;
import javax.enterprise.context.ApplicationScoped;
import javax.sql.DataSource;
import javax.transaction.TransactionSynchronizationRegistry;
import javax.transaction.Transactional;
@ApplicationScoped
public class TransactionalBean {
@Resource(lookup = "jdbc/mysql_local_1")
private DataSource ds;
@Resource
private TransactionSynchronizationRegistry tx;
@Transactional
public void required() throws Exception {
try (Connection con = ds.getConnection()) {
System.out.println("required() : txKey=" + tx.getTransactionKey());
}
}
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void requreisNew() throws Exception {
try (Connection con = ds.getConnection()) {
System.out.println("requreisNew() : txKey=" + tx.getTransactionKey());
}
}
}
情報: TransactionalEjb : txKey=JavaEETransactionImpl: txId=52 nonXAResource=827 jtsTx=null ...]
情報: requreisNew() : txKey=JavaEETransactionImpl: txId=53 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[]
情報: required() : txKey=JavaEETransactionImpl: txId=52 nonXAResource=827 jtsTx=null ...]
-
@Transactional
で CDI 管理ビーンのメソッド(クラス)をアノテートすることで、トランザクション境界を設けることができる。 - アノテーションの引数で、トランザクションの開始・終了をどうするかの制御ができる。
- 列挙型の
TxType
を使用する。 - それぞれの意味は、 EJB の場合 と同じ。
- 列挙型の
例外発生時の制御
※以下のコードは、最初 GlassFish 4.1 と Payara 4.1.153 で動作検証したのですが、どうも仕様通りに動いてくれなかったので Wildfly 9.0.1 で動作検証しました。
追記
コメント にあるとおり、バグだったようで今後修正されるようです。
基本
package sample.jta.ejb;
import javax.ejb.Stateless;
import javax.inject.Inject;
import sample.jta.cdi.TransactionalExceptionBean;
@Stateless
public class TransactionalExceptionEjb {
@Inject
private TransactionalExceptionBean bean;
public void execute() {
this.executeQuietly(this.bean::insertAndThrowException);
this.executeQuietly(this.bean::insertAndThrowRuntimeException);
}
private void executeQuietly(ThrowableRunnable tr) {
try {
tr.execute();
} catch (Exception ex) {
System.out.println("[" + ex.getClass().getSimpleName() + "] " + ex.getMessage());
}
}
@FunctionalInterface
private static interface ThrowableRunnable {
void execute() throws Exception;
}
}
package sample.jta.cdi;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.annotation.Resource;
import javax.enterprise.context.ApplicationScoped;
import javax.sql.DataSource;
import javax.transaction.Transactional;
@ApplicationScoped
@Transactional(Transactional.TxType.REQUIRES_NEW)
public class TransactionalExceptionBean {
@Resource(lookup = "java:/jdbc/mysql_test")
private DataSource ds;
public void insertAndThrowException() throws Exception {
this.insert(this.ds, "insertAndThrowException");
throw new Exception("test exception");
}
public void insertAndThrowRuntimeException() throws Exception {
this.insert(this.ds, "insertAndThrowRuntimeException");
throw new RuntimeException("test runtime exception");
}
private void insert(DataSource ds, String value) throws SQLException {
try (Connection con = ds.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO TEST_TABLE (VALUE) VALUES (?)");
) {
ps.setString(1, value);
ps.executeUpdate();
}
}
}
22:14:29,546 INFO [stdout] (default task-2) [Exception] test exception
22:14:29,552 INFO [stdout] (default task-2) [RuntimeException] test runtime exception
実行後のデータベース
- チェック例外がスローされた場合、トランザクションはロールバックされない。
- 非チェック例外がスローされた場合、トランザクションはロールバックされる。
この辺の挙動は、 EJB の場合と同じ。
ロールバックする(しない)例外を限定する
package sample.jta.cdi;
...
import java.util.function.Supplier;
@ApplicationScoped
@Transactional(Transactional.TxType.REQUIRES_NEW)
public class TransactionalExceptionBean {
@Resource(lookup = "java:/jdbc/mysql_test")
private DataSource ds;
...
@Transactional(
value=Transactional.TxType.REQUIRES_NEW,
rollbackOn=IOException.class,
dontRollbackOn=NullPointerException.class
)
public void insertAndThrowAnyException(Supplier<? extends Exception> exceptionProducer) throws Exception {
Exception e = exceptionProducer.get();
this.insert(this.ds, "insertAndThrowAnyException(" + e.getClass().getSimpleName() + ")");
throw e;
}
...
}
package sample.jta.ejb;
import java.io.IOException;
...
@Stateless
public class TransactionalExceptionEjb {
@Inject
private TransactionalExceptionBean bean;
public void execute() {
...
// RuntimeException
this.executeQuietly(() -> this.bean.insertAndThrowAnyException(NullPointerException::new));
this.executeQuietly(() -> this.bean.insertAndThrowAnyException(IllegalStateException::new));
// Exception
this.executeQuietly(() -> this.bean.insertAndThrowAnyException(NoSuchMethodException::new));
this.executeQuietly(() -> this.bean.insertAndThrowAnyException(IOException::new));
}
...
}
22:30:30,120 INFO [stdout] (default task-4) [NullPointerException] null
22:30:30,127 INFO [stdout] (default task-4) [IllegalStateException] null
22:30:30,132 INFO [stdout] (default task-4) [NoSuchMethodException] null
22:30:30,137 INFO [stdout] (default task-4) [IOException] null
実行後のデータベース
-
rollbackOn
で、チェック例外の中でもロールバックする例外を指定できる。-
IOException
はチェック例外なので、デフォルトではロールバックされずデータベースに登録される。 - しかし、
rollbackOn
で指定しているので、ロールバックされてデータベースには登録されていない。
-
-
dontRollbackOn
で、非チェック例外の中でもロールバックしない例外を指定できる。-
NullPointerException
は非チェック例外なので、デフォルトではロールバックされてデータベースには登録されない。 - しかし、
dontRollbackOn
で指定しているので、ロールバックされずにデータベースに登録されている。
-
GlassFish での動作
GlassFish で動かすと、 RuntimeException
をスローしてもロールバックしてくれませんでした。
代わりに、 RollbackException
がスローされるという現象が発生します。
挙動的には、 GlassFish4.1をなおしてみた - 見習いプログラミング日記 こちらの記事に描かれているものと同じ感じでした。
issue は reolved だけど、もしかして例外のセットだけ直して、 rollbackonly → commit の即死コンボは直ってないのかなぁ。。。
@TransactionScoped
package sample.jta.cdi;
import java.io.Serializable;
import javax.transaction.TransactionScoped;
@TransactionScoped
public class TransactionScopedBean implements Serializable {
@Override
public String toString() {
return "TransactionScopedBean {hash=" + this.hashCode() + "}";
}
}
package sample.jta.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.transaction.TransactionSynchronizationRegistry;
import sample.jta.cdi.TransactionScopedBean;
@Stateless
public class TransactionScopedEjb {
@Inject
private TransactionScopedBean bean;
@Resource
private TransactionSynchronizationRegistry tx;
@Inject
private RequiredEjb requiredEjb;
@Inject
private RequiresNewEjb requiresNewEjb;
public void execute() {
System.out.println("[TestTransactionScopedEjb]");
System.out.println("txKey=" + this.tx.getTransactionKey());
System.out.println("bean=" + this.bean);
this.requiresNewEjb.execute();
this.requiredEjb.execute();
}
}
package sample.jta.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.transaction.TransactionSynchronizationRegistry;
import sample.jta.cdi.TransactionScopedBean;
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class RequiredEjb {
@Inject
private TransactionScopedBean bean;
@Resource
private TransactionSynchronizationRegistry tx;
public void execute() {
System.out.println("[RequiredEjb]");
System.out.println("txKey=" + this.tx.getTransactionKey());
System.out.println("bean=" + this.bean);
}
}
package sample.jta.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.transaction.TransactionSynchronizationRegistry;
import sample.jta.cdi.TransactionScopedBean;
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public class RequiresNewEjb {
@Inject
private TransactionScopedBean bean;
@Resource
private TransactionSynchronizationRegistry tx;
public void execute() {
System.out.println("[RequiresNewEjb]");
System.out.println("txKey=" + this.tx.getTransactionKey());
System.out.println("bean=" + this.bean);
}
}
情報: [TestTransactionScopedEjb]
情報: txKey=JavaEETransactionImpl: txId=1 ...]
情報: bean=TransactionScopedBean {hash=1815003275}
情報: [RequiresNewEjb]
情報: txKey=JavaEETransactionImpl: txId=2 ...]
情報: bean=TransactionScopedBean {hash=2009619742}
情報: [RequiredEjb]
情報: txKey=JavaEETransactionImpl: txId=1 ...]
情報: bean=TransactionScopedBean {hash=1815003275}
-
txId
とTransactionScopedBean
のハッシュ値の関係に注目。 -
@TransactionScoped
でアノテートすると、トランザクションごとにインスタンスが生成される。
参考
- JSR 907: JavaTM Transaction API (JTA) | The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 907
- 1.4 Java Transaction Service(JTS)
- 分散トランザクションに挑戦しよう!
- トランザクションモニター - Wikipedia
- X/Open XA - Wikipedia
- X/Openとは 「X/Open Company, Ltd.」 (エックスオープン): - IT用語辞典バイナリ
- Oracle XAを使用したアプリケーションの開発
- The Open Groupとは ジオープングループ: - IT用語辞典バイナリ
- JDBC 接続プールの編集 (Sun GlassFish Enterprise Server 2.1 管理ガイド)
- Amazon.co.jp: マスタリングJavaEE5 第2版 (DVD付) (Programmer’s SELECTION): 三菱UFJインフォメーションテクノロジー株式会社 斉藤 賢哉: 本
- Amazon.co.jp: Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer’s SELECTION): Antonio Goncalves, 日本オラクル株式会社, 株式会社プロシステムエルオーシー: 本
- Amazon.co.jp: Enterprise JavaBeans 3.1 第6版: Andrew Lee Rubinger, Bill Burke, 佐藤 直生, 木下 哲也: 本
- Java Transaction API - Wikipedia