JavaEE Code探索 その2 〜 トランザクション 〜

f:id:Naotsugu:20170920225600p:plain

前回は Glassfish 4.1.2 のソースコードを元にして EJB のメソッド呼び出しの概要を見ました。

blog1.mammb.com

今回は、この流れの中でトランザクションの開始と終了について見ていきます。

前回と同様に今回の説明のために不要な箇所は大幅に省略または改編したものとなりますので注意してください。

コードは Glassfish 4.1.2 系のものです。


はじめに

EJB 呼び出しは(コンテナ管理トランザクションの場合)、トランザクション属性がデフォルトで REQUIRED になります。

メソッド呼び出し時にトランザクションが開始していなければ開始し、既に開始していればそのトランザクション内で実行する というやつです。

以下のように何もしなければトランザクション属性は REQUIRED として実行されます。

@Stateless
public class FooServiceImpl implements FooService {
    public void hoge(String arg) {
    }
}

明示的にトランザクション属性を指定する場合は、以下のようにアノテーションで指定します。

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void hoge(String arg) {
}

トランザクション属性は以下のように enum 定義されています。

public enum TransactionAttributeType {
    MANDATORY,
    REQUIRED,
    REQUIRES_NEW,
    SUPPORTS,
    NOT_SUPPORTED,
    NEVER;
}

トランザクション開始のアウトライン

大枠は以下のようになります。

f:id:Naotsugu:20170920225811p:plain

トランザクションの初期化

前回見てきたように、EJB のメッドコールは EJBObjectInvocationHandler が入り口です(ローカルコールの場合は EJBLocalObjectInvocationHandler)。

package com.sun.ejb.containers;

public final class EJBObjectInvocationHandler extends EJBObjectImpl implements InvocationHandler {

    Object invoke(Class clientInterface, Method method, Object[] args) {
        EjbInvocation inv = baseContainer.createEjbInvocation();
        // ...

        baseContainer.preInvoke(inv);

        Object returnValue = baseContainer.intercept(inv);

        baseContainer.postInvoke(inv);

        baseContainer.getSecurityManager().resetPolicyContext();
        return returnValue;
    }
}

トランザクション属性に応じた処理は、BaseContainer.preInvoke() で開始処理、BaseContainer.postInvoke() で終了処理が行われます。

最初に開始処理の中身 BaseContainer.preInvoke() から見ていきます。

トランザクションの開始 BaseContainer.preInvoke()

BaseContainer の事前処理は以下のようになっています。

package com.sun.ejb.containers;

public abstract class BaseContainer implements Container, EjbContainerFacade, JavaEEContainer {

    public void preInvoke(EjbInvocation inv) {

        inv.transactionAttribute = inv.invocationInfo.txAttr;
        inv.container = this;
        inv.setPreInvokeTxStatus(transactionManager.getStatus());
        invocationManager.preInvoke(inv);

        preInvokeTx(inv);

        inv.setPreInvokeTxStatus(null);
        enlistExtendedEntityManagers(ctx);
    }
}

EjbInvocation に TX 関連の情報を設定して preInvokeTx() を呼んでいます。

同クラス内の preInvokeTx() は以下のようになっており、

    protected final void preInvokeTx(EjbInvocation inv) throws Exception {
        if (inv.invocationInfo == null) {
            inv.invocationInfo = getInvocationInfo(inv);
            inv.transactionAttribute = inv.invocationInfo.txAttr;
        }
        containerTransactionManager.preInvokeTx(inv);
    }

コンテナの管理するトランザクションマネージャ containerTransactionManager.preInvokeTx() に処理を委譲しています。

このクラスは具体的には EJBContainerTransactionManager というクラスです。

トランザクションマネージャ

preInvokeTx() では、トランザクション属性に応じた switch 文にて各処理を行います。

package com.sun.ejb.containers;

public class EJBContainerTransactionManager {

    final void preInvokeTx(EjbInvocation inv) throws Exception {

        int txAttr = container.getTxAttr(inv);
        EJBContextImpl context = (EJBContextImpl)inv.context;
        Transaction prevTx = context.getTransaction();

        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;

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

            // ...
            default :
                throw new EJBException("Bad transaction attribute");
        }
    }
}

代表的なものだけ抜き出しています。

TX_REQUIRED の場合、トランザクションが開始していなければ startNewTx() でトランザクションを開始します。 そうでなければ useClientTx() でこのEJBの呼び出し元のトランザクションを利用します。

TX_REQUIRES_NEW の場合には、既存トランザクションをサスペンドして新たなトランザクションを開始することが分かるでしょう。

トランザクションの begin

startNewTx() は以下のような実装になります。

package com.sun.ejb.containers;

public class EJBContainerTransactionManager {

    private void startNewTx(Transaction prevTx, EjbInvocation inv) throws Exception {

        transactionManager.begin();

        EJBContextImpl context = (EJBContextImpl)inv.context;
        Transaction tx = transactionManager.getTransaction();
        context.setTransaction(tx);

        transactionManager.enlistComponentResources();
        container.afterBegin(context);
    }

トランザクションマネージャの begin() してから、Transaction を取得してコンテキストに設定しています。

ここでの transactionManager は JavaEETransactionManagerSimplified のインスタンスで、その begin() は以下のようになっています。

package com.sun.enterprise.transaction;

@Service
@ContractsProvided({TransactionManager.class, JavaEETransactionManager.class})
@Rank(Constants.DEFAULT_IMPLEMENTATION_RANK)
public class JavaEETransactionManagerSimplified implements JavaEETransactionManager, PostConstruct {

   public void begin() throws NotSupportedException, SystemException {
       begin(getEffectiveTimeout());
   }

   public void begin(int timeout) throws NotSupportedException, SystemException {
        setDelegate();
        initJavaEETransaction(timeout);
    }
}

initJavaEETransaction() では以下のように JavaEETransactionImpl を生成して現在のトランザクションに設定しています。

public class JavaEETransactionManagerSimplified implements JavaEETransactionManager, PostConstruct {
    private JavaEETransactionImpl initJavaEETransaction(int timeout) {
        JavaEETransactionImpl tx = null;
        if (timeout > 0)
            tx = new JavaEETransactionImpl(timeout, this);
        else
            tx = new JavaEETransactionImpl(this);

        setCurrentTransaction(tx); // transactions.set(t);
        return tx;
    }
}

setCurrentTransaction() ではスレッドローカルに生成したトランザクションを保存しています。

このように生成したトランザクションは、EJBContainerTransactionManager.startNewTx() でコンテキストに保存され、コンテキスト経由で利用できるようになります。

トランザクション終了のアウトライン

大枠は以下のようになります。

f:id:Naotsugu:20170921231407p:plain

トランザクションの後処理

再掲になりますが、EJBObjectInvocationHandler です。

package com.sun.ejb.containers;

public final class EJBObjectInvocationHandler extends EJBObjectImpl implements InvocationHandler {

    Object invoke(Class clientInterface, Method method, Object[] args) {
        EjbInvocation inv = baseContainer.createEjbInvocation();
        // ...

        baseContainer.preInvoke(inv);

        Object returnValue = baseContainer.intercept(inv);

        baseContainer.postInvoke(inv);

        baseContainer.getSecurityManager().resetPolicyContext();
        return returnValue;
    }
}

トランザクションの後処理は BaseContainer.postInvoke() で行われます。

では BaseContainer.postInvoke() の中身を見ていきます。

public abstract class BaseContainer implements Container, EjbContainerFacade, JavaEEContainer {

    public void postInvoke(EjbInvocation inv) {
        postInvoke(inv, true);
    }

    protected void postInvoke(EjbInvocation inv, boolean doTxProcessing) {

        inv.setDoTxProcessingInPostInvoke(doTxProcessing);
        delistExtendedEntityManagers(inv.context);
            
        if (doTxProcessing) {
            postInvokeTx(inv);
        }

        invocationManager.postInvoke(inv);
        releaseContext(inv);

        if (inv.exception != null) {
            // ...
        }
    }

    protected void postInvokeTx(EjbInvocation inv) throws Exception {
        containerTransactionManager.postInvokeTx(inv);
    }
}

コンテナの管理するトランザクションマネージャ containerTransactionManager.postInvokeTx() に処理を委譲しています。

このあたりは開始時と同じで EJBContainerTransactionManager で処理されることになります。

トランザクションマネージャ

postInvokeTx() では、トランザクション属性に応じた switch 文にて各処理を行います。

代表的なものだけ以下に示します。このあたりの流れも開始時と同じですね。

public class EJBContainerTransactionManager {

    protected void postInvokeTx(EjbInvocation inv) throws Exception {

        Throwable exception = inv.exception;

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

        Throwable newException = inv.exception;

        switch (txAttr) {

            case Container.TX_REQUIRED:
                if (inv.clientTx == null) {
                    newException = completeNewTx(context, exception, status);
                } else {
                    if (exception != null) {
                        newException = checkExceptionClientTx(context, exception);
                    }
                }
                break;

            case Container.TX_REQUIRES_NEW:
                newException = completeNewTx(context, exception, status);

                if (inv.clientTx != null) {
                    transactionManager.resume(inv.clientTx);
                }
                break;

            default:
        }
        inv.exception = newException;
     }
}

TX_REQUIRED の場合、EJB 呼び出し元がトランザクションに参加していなければ completeNewTx() でトランザクションを完了させます。

TX_REQUIRES_NEW の場合には、今回のトランザクションを完了し、EJB 呼び出し元がトランザクションに参加していれば resume() して元のトランザクションに戻る処理となつています。

completeNewTx() は以下のようになっています。

public class EJBContainerTransactionManager {

    private Throwable completeNewTx(EJBContextImpl context, Throwable exception, int status) throws Exception {
        Throwable newException = // ...

        if ( container.isStatefulSession && (context instanceof SessionContextImpl)) {
            ((SessionContextImpl) context).setTxCompleting(true);
        }

        if (newException != null && container.isSystemUncheckedException(newException)) {
            destroyBeanAndRollback(context, null);
            newException = processSystemException(newException);

        } else {
            if (status == Status.STATUS_MARKED_ROLLBACK) {
                transactionManager.rollback();
            } else {
                if (newException != null && isAppExceptionRequiringRollback(newException)) {
                    transactionManager.rollback();
                } else {
                    transactionManager.commit();
                }
            }
        }
        return newException;
    }

状態に応じてトランザクションマネージャの commit() または rollback() しています。

ここでの transactionManager は JavaEETransactionManagerSimplified のインスタンスで、その commit() は以下のようになっています。

トランザクションマネージャの commit

基本的には開始時にスレッドローカルに保存した JavaEETransaction を取得して commit() となります。

@Service
@ContractsProvided({TransactionManager.class, JavaEETransactionManager.class})
@Rank(Constants.DEFAULT_IMPLEMENTATION_RANK)
public class JavaEETransactionManagerSimplified implements JavaEETransactionManager, PostConstruct {

    public void commit() throws RollbackException,
            HeuristicMixedException, HeuristicRollbackException, SecurityException,
            IllegalStateException, SystemException {
        
        try {
            JavaEETransaction tx = transactions.get();
            if (tx != null && tx.isLocalTx()) {
                tx.commit();
            } else {
                try {
                    getDelegate().commitDistributedTransaction(); 
                } finally {
                    if (tx != null) {
                        ((JavaEETransactionImpl)tx).onTxCompletion(true);
                    }
                }
            }

        } finally {
            setCurrentTransaction(null);
            delegates.set(null);
        }
    }
}

finally 句において、setCurrentTransaction() でスレッドローカルに保存したトランザクションをクリアしています。

また、開始時にコンテキストに設定したトランザクションは、BaseContainer.postInvoke() から以下が呼ばれてクリアされます。

package com.sun.ejb.containers;

public class StatelessSessionContainer extends BaseContainer {

    public void releaseContext(EjbInvocation inv) {
        SessionContextImpl sc = (SessionContextImpl)inv.context;

        sc.setState(EJBContextImpl.BeanState.POOLED);
        sc.setTransaction(null);
        sc.touch();

        pool.returnObject(sc);
    }
}

まとめ

駆け足でしたが、

  • EJB のトランザクションは、BaseContainer.preInvoke() の中で開始し、BaseContainer.postInvoke() の中で終了する
  • トランザクション開始時には JavaEETransaction が生成されて SessionContext に設定される
  • トランザクション終了時には SessionContext に設定された JavaEETransaction がクリアされる
  • トランザクション属性に応じた処理は EJBContainerTransactionManager で扱われる

次回はトランザクションの suspend と resume についてもう少し詳細を説明しようと思います。