JavaEE Code探索 その3 〜 トランザクション属性 〜


f:id:Naotsugu:20170926230726p:plain

前回はEJBにおけるトランザクションの開始と終了の流れを見てきました。

etc9.hatenablog.com

今回は、トランザクション属性による動作を少しだけみてみましょう。

はじめに

TransactionAttributeType.REQUIRED のEJBメソッドから、TransactionAttributeType.REQUIRES_NEW の別EJBのメソッドを呼び出すケースを例として見ていきます。

最初の EJB は以下のような感じです。

@Stateless
public class Service1Impl implements Service1 {

    @EJB
    private Service2 service2;

    @Override
    public void exec() {
        service2.exec();
    }
}

このメソッド内で service2.exec() と別 EJB のメソッドを呼び出します。

@Stateless
public class ServiceBImpl implements ServiceB {

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void exec() {
        // ...
    }
}

呼び出し先は TransactionAttributeType.REQUIRES_NEW でアノテートされており、こちらが別トランザクションで実行される。

といった例です。

最初のトランザクション

Service1 側ではデフォルトで TransactionAttributeType.REQUIRED として処理されます。

この場合、前回見てきたものと同じく、EJBContainerTransactionManager.preInvokeTx() でトランザクション属性に応じた処理が行われます。

package com.sun.ejb.containers;

public class EJBContainerTransactionManager {

    final void preInvokeTx(EjbInvocation inv) throws Exception {

        EJBContextImpl context = (EJBContextImpl)inv.context;
        Transaction prevTx = context.getTransaction();

        int txAttr = container.getTxAttr(inv);
        switch (txAttr) {
            case Container.TX_REQUIRED:
                if (status == Status.STATUS_NO_TRANSACTION) {
                    inv.clientTx = null;
                    startNewTx(prevTx, inv);
                } else {
                    inv.clientTx = transactionManager.getTransaction();
                    useClientTx(prevTx, inv);
                }
                break;
            // ...
        }
    }
}

現在のトランザクションは無いので、startNewTx() で新しいトランザクションが作成されます。

新しいトランザクションは、前回見た通り、

  • JavaEETransactionImpl のインスタンスを作成
  • JavaEETransactionManagerSimplified のスレッドローカルにトランザクションを保存
  • EjbInvocation が保持するコンテキストにトランザクションを保存

という流れです。

次のトランザクション

TransactionAttributeType.REQUIRED の EJB メソッドから、TransactionAttributeType.REQUIRES_NEW の 別EJBメソッドを呼び出した場合も、同じように EJBContainerTransactionManager.preInvokeTx() で以下のように処理されます。

package com.sun.ejb.containers;

public class EJBContainerTransactionManager {

    final void preInvokeTx(EjbInvocation inv) throws Exception {
        // ...
        switch (txAttr) {
            case Container.TX_REQUIRES_NEW:
                if (status != Status.STATUS_NO_TRANSACTION) {
                    inv.clientTx = transactionManager.suspend();
                }
                startNewTx(prevTx, inv);
                break;
            // ...
        }
    }
}

トランザクションがあれば、transactionManager.suspend() でトランザクションをサスペンドし、

EjbInvocation の clientTx、つまり今回のEJB呼び出しのクライアントのトランザクションとして保存しています。

トランザクションのサスペンド

transactionManager.suspend() は何をしているのでしょうか?

JavaEETransactionManagerSimplified.suspend() で以下のように処理されています。

public class JavaEETransactionManagerSimplified 
        implements JavaEETransactionManager, PostConstruct {

    public Transaction suspend() throws SystemException {
        return getDelegate().suspend(transactions.get());
    }
}

委譲しているだけですね。

委譲先は、JTS の場合は JavaEETransactionManagerJTSDelegate となります。

@Service
public class JavaEETransactionManagerJTSDelegate 
            implements JavaEETransactionManagerDelegate, PostConstruct {
    
    private JavaEETransactionManager javaEETM;

    public Transaction suspend(JavaEETransaction tx) throws SystemException {
        if (!tx.isLocalTx()) suspendXA();

        javaEETM.setCurrentTransaction(null);
        return tx;
    }
}

javaEETM.setCurrentTransaction(null) で現在のトランザクションを空更新しています(JavaEETransactionManager のスレッドローカルを空更新しています)。

つまり、サスペンドとは現在のトランザクションを空に設定する。ということになります。

その後、前述の EJBContainerTransactionManager.preInvokeTx() の中で EJBContainerTransactionManager.startNewTx() として新しいトランザクションが作成して、作成したトランザクションを現在のトランザクションとして設定して利用します。

REQUIRES_NEW トランザクションの終了

TransactionAttributeType.REQUIRES_NEW でアノテートされたメソッドを抜ける時の処理はどのようになるでしょうか。

EJBContainerTransactionManager.postInvokeTx() で処理されます。

public class EJBContainerTransactionManager {

    protected void postInvokeTx(EjbInvocation inv) throws Exception {

        EJBContextImpl context = (EJBContextImpl)inv.context;
        int status = transactionManager.getStatus();
        int txAttr = inv.invocationInfo.txAttr;

        switch (txAttr) {
            case Container.TX_REQUIRES_NEW:
                newException = completeNewTx(context, exception, status);
                if (inv.clientTx != null) {
                    transactionManager.resume(inv.clientTx);
                }
                break;
        }

    }
}

開始時と同じような流れですね。

completeNewTx() でトランザクションを終了し、開始時に設定した呼び出し元のトランザクション(inv.clientTx) でレジュームしています。

具体的には JavaEETransactionManagerSimplified.resume() で以下のような処理が行われます。

public class JavaEETransactionManagerSimplified 
        implements JavaEETransactionManager, PostConstruct {

    public void resume(Transaction tobj) throws InvalidTransactionException, 
            IllegalStateException, SystemException {

        JavaEETransaction tx = transactions.get();
        if ( tobj instanceof JavaEETransactionImpl ) {
            JavaEETransactionImpl javaEETx = (JavaEETransactionImpl)tobj;
            if ( !javaEETx.isLocalTx() )
                getDelegate().resume(javaEETx.getJTSTx());

            setCurrentTransaction(javaEETx);
        }
        else {
            getDelegate().resume(tobj);
        }
    }
}

setCurrentTransaction() で呼び出し元のトランザクションを戻しているだけです。

REQUIRES_NEW とは

結局のところ、現在のトランザクションをサスペンドで空に更新し、新しいトランザクションを開始

終わったら現在のトランザクションを元のトランザクションで更新

というだけの話になります。 たまにEJBのメソッドがネストされているのでトランザクションもネストされていると勘違いされる人もいますが、単に現在のトランザクションは横に置いておいて新しいトランザクションを開始しているだけです。

その他のトランザクション属性

今まで見てきたように、トランザクション属性別の挙動は EJBContainerTransactionManager.preInvokeTx() を見れば一目瞭然です。

MANDATORY

呼出し元で開始されているトランザクションで実行。

トランザクションが開始されていなければ例外。

case Container.TX_MANDATORY:
    if ( isNullTx || status == Status.STATUS_NO_TRANSACTION ) {
        throw new TransactionRequiredLocalException();
    }
    useClientTx(prevTx, inv);
    break;
REQUIRED

トランザクションが開始していない場合は新しいトランザクションを開始

トランザクションが開始していればそのトランザクションで実行。

case Container.TX_REQUIRED:
    if ( isNullTx ) {
        throw new TransactionRequiredLocalException();
    }

    if ( status == Status.STATUS_NO_TRANSACTION ) {
        inv.clientTx = null;
        startNewTx(prevTx, inv);
    } else { // There is a client Tx
        inv.clientTx = transactionManager.getTransaction();
        useClientTx(prevTx, inv);
    }
    break;
REQUIRES_NEW

トランザクションが開始していればサスペンド

新しいトランザクションを開始

case Container.TX_REQUIRES_NEW:
    if ( status != Status.STATUS_NO_TRANSACTION ) {
        inv.clientTx = transactionManager.suspend();
    }
    startNewTx(prevTx, inv);
    break;
SUPPORTS

トランザクションが開始していればそのトランザクションで実行

トランザクションが開始されていなければトランザクション無しで実行

case Container.TX_SUPPORTS:
    if ( isNullTx ) {
        throw new TransactionRequiredLocalException();
    }

    if ( status != Status.STATUS_NO_TRANSACTION ) {
        useClientTx(prevTx, inv);
    } else { // we need to invoke the EJB with no Tx.
        container.checkUnfinishedTx(prevTx, inv);
        container.preInvokeNoTx(inv);
    }
    break;
NOT_SUPPORTED

トランザクションが開始されている場合はサスペンド

トランザクションが開始していなければそのまま実行

case Container.TX_NOT_SUPPORTED:
    if ( status != Status.STATUS_NO_TRANSACTION ) {
        inv.clientTx = transactionManager.suspend();
    }
    container.checkUnfinishedTx(prevTx, inv);
    container.preInvokeNoTx(inv);
    break;
NEVER

トランザクションが開始されている場合は例外

トランザクションが開始していなければそのまま実行

case Container.TX_NEVER:
    if ( isNullTx || status != Status.STATUS_NO_TRANSACTION ) {
        throw new EJBException("EJB cannot be invoked in global transaction");
    } else { // we need to invoke the EJB with no Tx.
        container.checkUnfinishedTx(prevTx, inv);
        container.preInvokeNoTx(inv);
    }
    break;


日本語で説明されるより、コードで見る方が分かりやすいですね。