JavaEE Code探索 その1 〜 EJB コール 〜


f:id:Naotsugu:20170909140108p:plain

Glassfish のソースコードを元に、リモートEJBコールがどのように処理されていくかを説明します。

トラブルシュートにはどうしてもソースコードを読む必要がありますし、設定の問題なのかバグなのかの切り分けも容易になります。手っ取り早いし確実です。 そういった意味で、コードの概要を把握しておくことはフレームワークを使う上で重要なことがらです。

Glassfish ではどのようにしてリモートEJBのメソッド呼び出しが処理されているかの概要を示すことを目的とするため、今回の説明のために不要な箇所は大幅に省略しています。 つまり概要を把握しやすいようにソースコードは改編したものとなりますので注意してください。

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

アウトライン

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

f:id:Naotsugu:20170915190529p:plain

もとにするEJB

以下のような リモートの SSB を対象にします。

@Remote
public interface FooService {
    viod hoge(String atg);
}

SSB の実装は以下のようなものを想定します。

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

Foo サービスの hoge メソッドがどのように呼び出されるかについて見ていきます。

リモート・オブジェクトのコール(クライアント)

EJB は分散アプリケーションに向けたものであり、リモート・オブジェクトをローカル・オブジェクトと同じように扱うことができます。 裏の仕組みとしては、 EJB は ORB (Object Request Broker) を元にしてクライアントから呼び出されます。

コンテナにより自動生成された ORB 通信用のオブジェクトを経由することでリモートコールの通信が処理されるますが、ここではその詳細については踏み込みません。

例えば以下のような(EJBの)クライアントコードがあった場合、

@Path("/")
public class FooResource {
    @EJB
    private FooService fooSrvice;
}

fooSrvice には _FooService_Wrapper といった自動生成されたプロキシクラスが DI されます。

このサービスのメソッド呼び出しは、_FooService_DynamicStub といった、こちらも自動生成された ORB 通信用のスタブクラスに委譲されます。

このスタブからのメソッド呼び出しは以下で処理されます。

package com.sun.corba.ee.impl.presentation.rmi.codegen;

public class CodegenStubBase extends Stub {

    protected Object invoke(int methodNumber, Object[] args) {
        Method method = this.methods[methodNumber];
        return this.handler.invoke((Object)null, method, args);
    }
}

ハンドラの invoke()method を引数にコールします。

method_FooService_Remote という自動生成されたクラスの hoge() メソッドになります。

その結果、実際に呼び出される該当コードは以下になります。

package com.sun.corba.ee.impl.presentation.rmi;

public final class StubInvocationHandlerImpl implements LinkedInvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args) {
        interceptor.preInvoke();
        Object ret = this.privateInvoke(delegate, proxy, method, args);
        interceptor.postInvoke();
        return ret;
    }
}

preInvoke()postInvoke() の間で privateInvoke() を呼びます。

    private Object privateInvoke(Delegate delegate, Object proxy, final Method method, Object[] args) {

        DynamicMethodMarshaller dmm = this.pm.getDynamicMethodMarshaller(method);
        ORB orb = delegate.orb(this.stub);
        ServantObject so = delegate.servant_preinvoke(this.stub, giopMethodName, method.getDeclaringClass());
        Object[] copies = dmm.copyArguments(args, orb);

        Object result = method.invoke(so.servant, copies);

        return dmm.copyResult(result, orb);
    }

各変数の具体的なインスタンスは以下となります。

  • delegate : FooService
  • method : _FooService_Remote.hoge()
  • pm : PresentationManagerImpl

method_FooService_Remote.hoge() のメソッドコールとして ORB 経由でリモートのEJBコールが行われます。

リモートコールの受け口

EJBコールは EJBObjectInvocationHandlerDelegate が入り口になります。

package com.sun.ejb.containers;

public class EJBObjectInvocationHandlerDelegate implements InvocationHandler {

    private Class remoteBusinessIntfClass;
    private EJBObjectInvocationHandler delegate;

    public Object invoke(Object proxy, Method method, Object[] args) {
        return delegate.invoke(remoteBusinessIntfClass, method, args);
    }
}

このクラスは EJB のリモートインターフェースプロキシと EJBObjectInvocationHandler の単純なアダプタとなっています。

各変数の具体的なインスタンスは以下となります。

  • remoteBusinessIntfClass : ClientDelegateImpl
  • delegate : EJBObjectInvocationHandler
  • method : _FooService_Remote.hoge()

EJBObjectInvocationHandler.invoke() に処理を委譲します。

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;
    }

このクラスが EJB コールの実質的な入り口です。

EJBObjectInvocationHandler は以下のようなクラス階層になっています。

  • abstract class EJBLocalRemoteObject
    • abstract class EJBObjectImpl extends EJBLocalRemoteObject implements EJBObject
      • class EJBObjectInvocationHandler extends EJBObjectImpl implements InvocationHandler

baseContainer.createEjbInvocation() により EjbInvocation を生成し、このクラスに EJB 起動に必要な情報が設定されます。

作成した EjbInvocation を引数にして baseContainer.intercept(inv) をコールします。

BaseContainer

BaseContainer クラスは以下のクラスの親クラスです。

  • StatefulSessionContainer
  • StatelessSessionContainer
  • MessageBeanContainer
  • CMCSingletonContainer
  • BMCSingletonContainer

BaseContainer.intercept(inv) は以下のようになります。

package com.sun.ejb.containers;

public abstract class BaseContainer implements Container, EjbContainerFacade, JavaEEContainer {

    protected Object intercept(EjbInvocation inv) {

        Object result = null;

        if (inv.mustInvokeAsynchronously()) {

            EjbAsyncInvocationManager asyncManager = ((EjbContainerUtilImpl) ejbContainerUtilImpl).getEjbAsyncInvocationManager();
            Future future = inv.isLocal 
                ? asyncManager.createLocalFuture(inv) 
                : asyncManager.createRemoteFuture(inv, this, (GenericEJBHome) ejbRemoteBusinessHomeStub);
            return (inv.invocationInfo.method.getReturnType() == void.class) ? null : future;

        } else {

            onEjbMethodStart(inv.invocationInfo.str_method_sig);
            result = interceptorManager.intercept(inv.getInterceptorChain(), inv);
            onEjbMethodEnd(inv.invocationInfo.str_method_sig,  inv.exception);

        }
        return result;
    }
}

非同期と同期のコールをここで分岐しています。今回の場合は else 側の同期コールになります。

InterceptorManager.intercept() にてEJBメソッドコール時のインタセプトを処理します。

inv.getInterceptorChain() ではインタセプターを順に実行する InterceptorChain が得られます。

インタセプタチェーンはコンテナスタート時の以下の BaseContainer 内で InvocationInfo として格納されており、EJBObjectInvocationHandler の中で EjbInvocation に設定されたものです。

BaseContainer ではコンテナスタート時に以下のように InterceptorChain が作成されます。

public final void setStartedState() {

    initializeInterceptorManager();

    for(Object o : invocationInfoMap.values()) {
        InvocationInfo next = (InvocationInfo) o;
        setInterceptorChain(next);
    }
    for(Object o : this.webServiceInvocationInfoMap.values()) {
        InvocationInfo next = (InvocationInfo) o;
        setInterceptorChain(next);
    }

    containerState = CONTAINER_STARTED;

}

インタセプタチェーン

インタセプタチェーンは以下から開始します。

package com.sun.ejb.containers.interceptors;

public class InterceptorManager {

    public Object intercept(InterceptorManager.InterceptorChain chain, AroundInvokeContext ctx) {
        return chain.invokeNext(0, ctx);
    }
}

InterceptorChain.invokeNext() でインタセプタ順番に適用していく処理となります。 第一引数がインタセプタのインデックスとなっており、順番に実行していきます(開始時なので0)。

package com.sun.ejb.containers.interceptors;

class AroundInvokeChainImpl implements InterceptorManager.InterceptorChain {
    public Object invokeNext(int index, InterceptorManager.AroundInvokeContext inv) {
        return (index < size) 
            ? interceptors[index].intercept(inv) 
            : inv.invokeBeanMethod();
    }
}

インタセプタがあれば実行、無くなったら EjbInvocation.invokeBeanMethod() で EJB メソッドをコールという流れです。

この時 interceptors[] には AroundInvokeInterceptor のインスタンスが 2つ入っています

  • SystemInterceptorProxy へ処理を委譲するもの
  • SessionBeanInterceptor へ処理を委譲するもの

interceptors[index].intercept(inv) のコールは以下のように処理します。

package com.sun.ejb.containers.interceptors;

class AroundInvokeInterceptor {
    Object intercept(final InterceptorManager.AroundInvokeContext invCtx) {
        final Object[] interceptors = invCtx.getInterceptorInstances();
        return method.invoke(interceptors[index], invCtx);
    }
}

最初のインタセプタでは SystemInterceptorProxy のメソッドをコールします。

package com.sun.ejb.containers.interceptors;

public class SystemInterceptorProxy {
    
    @AroundInvoke
    public Object aroundInvoke(InvocationContext ctx) throws Exception {
        return doCall(ctx, aroundInvoke);
    }

    private Object doCall(InvocationContext ctx, Method m) throws Exception {
        Object returnValue = null;
        if (delegate != null && m != null) {
            returnValue = m.invoke(delegate, ctx);
        } else {
            returnValue = ctx.proceed();
        }
        return returnValue;
    }

doCall() では委譲先があれば委譲メソッドコール、そうでなければ EjbInvocation.proceed() をコールします。

今回は委譲先が無いため EjbInvocation.proceed() の処理に移ります。

package com.sun.ejb;

public class EjbInvocation extends ComponentInvocation
    implements InvocationContext, TransactionOperationsManager, 
         org.glassfish.ejb.api.EJBInvocation, InterceptorManager.AroundInvokeContext
                 
    public Object proceed() {
        interceptorIndex++;
        return getInterceptorChain().invokeNext(interceptorIndex, this);
    }

EjbInvocation ではインタセプタのインデックスをインクリメントし、次のインタセプタを処理します。

2 つ目のインタセプタとして CDI用 の SessionBeanInterceptor が入っており、これをコールします。

package org.jboss.weld.ejb;

public class SessionBeanInterceptor extends AbstractEJBRequestScopeActivationInterceptor implements Serializable {

    public Object aroundInvoke(InvocationContext invocation) {
        return super.aroundInvoke(invocation);
    }
}

親の aroundInvoke() をコールしており、

package org.jboss.weld.ejb;

public abstract class AbstractEJBRequestScopeActivationInterceptor implements Serializable {
    
    public Object aroundInvoke(InvocationContext invocation) throws Exception {
        if (this.isRequestContextActive()) {
            // リクエストスコープのコンテキストが有効
            return invocation.proceed();
        } else {
            EjbRequestContext requestContext = this.getEjbRequestContext();

            Object ret;
            requestContext.associate(invocation);
            requestContext.activate();

            ret = invocation.proceed();
            requestContext.invalidate();
            requestContext.deactivate();
            return ret;
        }
    }
}

CDI のリクエストコンテキストの処理を行うメソッドとなります。

今回はリクエストスコープのコンテキストが既に有効なので EjbInvocation.proceed() をコールするのみで終わります。

前述の通り、次のインタセプタチェーンのコールに戻り、

public Object proceed() {
    interceptorIndex++;
    return getInterceptorChain().invokeNext(interceptorIndex, this);
}

AroundInvokeChainImpl では、

public Object invokeNext(int index, InterceptorManager.AroundInvokeContext inv)  {
    return (index < size) 
        ? interceptors[index].intercept(inv) 
        : inv.invokeBeanMethod();
    }
}

インタセプタを実行し尽くして EjbInvocation.invokeBeanMethod() をコールします。

EJB メソッドのコール

EjbInvocation.invokeBeanMethod() の処理です。

package com.sun.ejb;

public class EjbInvocation extends ComponentInvocation // ...
{                
    public Object invokeBeanMethod() {
        return ((BaseContainer) container).invokeBeanMethod(this);
    }
}

BaseContainer で自身(EjbInvocation) を引数に invokeBeanMethod() をコールします。

package com.sun.ejb.containers;

public abstract class BaseContainer
    implements Container, EjbContainerFacade, JavaEEContainer {

    public Object invokeBeanMethod(EjbInvocation inv) {
        return securityManager.invoke(
            inv.getBeanMethod(), inv.isLocal, inv.ejb, inv.getParameters());
    }
}

BaseContainer では EJBSecurityManager に処理を委譲してEJBコールを行います。

EJBSecurityManager では、

package org.glassfish.ejb.security.application;

public final class EJBSecurityManager implements SecurityManager {

    public Object invoke(Method beanClassMethod, boolean isLocal, Object obj, Object[] objArr) {
        return this.runMethod(beanClassMethod, obj, objArr);
    }

    public Object runMethod(Method beanClassMethod, Object obj, Object[] oa) {
        String oldCtxID = setPolicyContext(this.contextId);
        Object ret = beanClassMethod.invoke(obj, oa);
        resetPolicyContext(oldCtxID, this.contextId);
        return ret;
    }
}

JACC のコンテキストの処理を挟んで、beanClassMethod.invoke() により EJBのサービスメソッドをコールします。

beanClassMethodFooServiceImpl.hoge() であり、リフレクション経由でようやく EJB のメソッドのコール となります。

まとめ

  • EJBコールは EJBObjectInvocationHandler が実質的な入り口
  • BaseContainer が主役
  • EjbInvocation が EJB 起動時の情報や状態を管理する
  • InterceptorManager およびその内部クラス AroundInvokeChain がEJBコール時のインタセプタで各種処理を挟む
  • 最終的な EJBメソッドのコールは EJBSecurityManager により行われる