21.5. サービス、分離インボーカーへのリモートアクセス

任意の機能性を統合することが可能な MBean サービスの概念に加えて、JBoss には分離インボーカーの概念もあります。その概念とは MBean サービスがクライアントからのリモートアクセスに対し任意のプロトコルを通じて機能的なインターフェースを公開できるということです。分離インボーカーの概念は、サービスがアクセスされるリモーティングとプロトコルがコンポーネントから独立した機能的な側面またはサービスであるということです。従って、ネーミングサービスを RMI/JRMP、RMI/HTTP、RMI/SOAP またはどの任意のカスタムトランスポートを通じても使用可能にできます。
関係するコンポーネントの概要から分離インボーカーアーキテクチャに関する話を始めましょう。分離インボーカーアーキテクチャの主要コンポーネントは 図21.1「分離インボーカーアーキテクチャの主要コンポーネント」 に表示されています。
分離インボーカーアーキテクチャの主要コンポーネント

図21.1 分離インボーカーアーキテクチャの主要コンポーネント

クライアント側には、MBean サービスのインターフェースを公開するクライアントプロキシが存在します。これは EJB ホームとリモートインターフェースに使用される同じのスマートなコンパイルのない動的プロキシです。任意のサービスに対するプロキシと EJB との唯一の違いは、公開されたインターフェースのセットだけでなく、プロキシの中にあるクライアント側インターセプタです。クライアントインターセプタはクライアントプロキシの中にある四角形で表示されています。インターセプタはメソッド呼び出し / 戻り値の変換を可能にするパターンのアセンブリラインのタイプです。クライアントは検索メカニズム、通常は JNDI を通じてプロキシを取得します。 RMI は 図21.1「分離インボーカーアーキテクチャの主要コンポーネント」 に示されていますが、公開されたインターフェースとそのタイプの唯一の実際の要件は、JNDI のクライアントサーバーだけでなくトランスポートレイヤの間でシリアル化が可能ということです。
トランスポートレイヤの選択は、クライアントプロキシの最後のインターセプタで決まります。それは、図21.1「分離インボーカーアーキテクチャの主要コンポーネント」インボーカーインターセプタと呼ばれます。インボーカーインターセプタにはサーバー側の分離インボーカー MBean サービスのトランスポート固有スタブに関する参照が含まれます。インボーカーインターセプタは目的の MBean と同じ VM 内で発生する呼び出しの最適化も処理します。インボーカーインターセプタがこれが該当ケースとして検出すると、単に目的の MBean に呼び出しを渡す参照渡しインボーカーに呼び出しが渡されます。
分離インボーカーサービスの役割は、分離インボーカーが処理するトランスポートにより汎用呼び出し操作を使用可能にすることです。Invoker インターフェースは汎用呼び出し操作を図解します。
package org.jboss.invocation;
            
import java.rmi.Remote;
import org.jboss.proxy.Interceptor;
import org.jboss.util.id.GUID;
            
            
public interface Invoker
    extends Remote
{
    GUID ID = new GUID();

    String getServerHostName() throws Exception;

    Object invoke(Invocation invocation) throws Exception;
}
Invoker インターフェースは Remote を拡張し RMI と互換性を持たせますが、これはインボーカーが RMI サービススタブを公開する必要があるという意味ではありません。分離インボーカーサービスは、特定のトランスポートに対して org.jboss.invocation.Invocation オブジェクトとして表される呼び出しを受け入れるトランスポートゲートウェイとして働きます。インボーカーサービスは呼び出しをアンマーシャリングし、呼び出しを 図21.1「分離インボーカーアーキテクチャの主要コンポーネント」Target MBean で表されるデスティネーション MBean サービスに転送し、クライアントへの転送コールバックから生じる戻り値または例外をマーシャリングします。
Invocation オブジェクトは単にメソッド呼び出しコンテキストを表したものです。これには目的の MBean 名、メソッド、メソッド引数、プロキシファクトリによりプロキシに関連付けられた情報のコンテキスト、クライアントプロキシインターセプタにより呼び出しと関連付けられたデータの任意のマップが含まれます。
クライアントプロキシの設定はサーバー側プロキシファクトリ MBean サービスで行われ、図21.1「分離インボーカーアーキテクチャの主要コンポーネント」プロキシファクトリコンポーネントで示されています。プロキシファクトリは次のタスクを実行します。
  • 目的の MBean が公開したいインターフェースを実装する動的プロキシを作成します。
  • クライアントプロキシインターセプタと動的プロキシハンドラを関連付けます。
  • 呼び出しコンテキストと動的プロキシを関連付けます。これには目的の MBean、分離インボーカースタブ、プロキシ JNDI 名が含まれます。
  • プロキシを JNDI にバインドすることによりクライアントに対しプロキシを使用可能にします。
図21.1「分離インボーカーアーキテクチャの主要コンポーネント」 の最後のコンポーネントは、呼び出しのインターフェースをリモートクライアントに公開したい目的の MBean サービスです。特定のインターフェースを通じて MBean サービスがアクセス可能となるために必要なステップは以下のとおりです。
  • シグネチャと一致する JMX 操作を定義します。public Object invoke(org.jboss.invocation.Invocation) throws Exception
  • org.jboss.invocation.MarshalledInvocation.calculateHash メソッドを使用して、公開されたインターフェース java.lang.reflect.Method から長いハッシュ表現にマップする HashMap<Long, Method> を作成します。
  • invoke(Invocation) JMX 操作を実装し、インターフェースメソッドのハッシュマッピングを使用して、呼び出されたメソッドの長いハッシュ表現から公開されたインターフェースの java.lang.reflect.Method に変換します。リフレクションを使用して、公開されたインターフェースを実際に実装する MBean サービスと関連付けられたオブジェクトに関する実際の呼び出しを実行します。

21.5.1. 分離インボーカーの例、MBeanServer インボーカーアダプターサービス

MBean サービスにリモートアクセスを提供する必要があるステップの例として、本項では RMI/JRMP によるアクセスのための org.jboss.jmx.connector.invoker.InvokerAdaptorService とその設定について記載しています。

例21.1 InvokerAdaptorService MBean

InvokerAdaptorService は分離インボーカーパターンで目的の MBean の役割を果たすために存在する簡易 MBean サービスです。
package org.jboss.jmx.connector.invoker;
public interface InvokerAdaptorServiceMBean
    extends org.jboss.system.ServiceMBean
{
    Class getExportedInterface();
    void setExportedInterface(Class exportedInterface);

    Object invoke(org.jboss.invocation.Invocation invocation)
        throws Exception;
}

package org.jboss.jmx.connector.invoker;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.jboss.invocation.Invocation;
import org.jboss.invocation.MarshalledInvocation;
import org.jboss.mx.server.ServerConstants;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.system.Registry;

public class InvokerAdaptorService
    extends ServiceMBeanSupport
    implements InvokerAdaptorServiceMBean, ServerConstants
{
    private static ObjectName mbeanRegistry;
    
    static {
        try {
            mbeanRegistry = new ObjectName(MBEAN_REGISTRY);
        } catch (Exception e) {
            throw new RuntimeException(e.toString());
        }
    }

    private Map marshalledInvocationMapping = new HashMap();
    private Class exportedInterface;

    public Class getExportedInterface()
    {
        return exportedInterface;
    }

    public void setExportedInterface(Class exportedInterface)
    {
        this.exportedInterface = exportedInterface;
    }

    protected void startService()
        throws Exception
    {
        // Build the interface method map
        Method[] methods = exportedInterface.getMethods();
        HashMap tmpMap = new HashMap(methods.length);
        for (int m = 0; m < methods.length; m ++) {
            Method method = methods[m];
            Long hash = new Long(MarshalledInvocation.calculateHash(method));
            tmpMap.put(hash, method);
        }

        marshalledInvocationMapping = Collections.unmodifiableMap(tmpMap);
        // Place our ObjectName hash into the Registry so invokers can
        // resolve it
        Registry.bind(new Integer(serviceName.hashCode()), serviceName);
    }

    protected void stopService()
        throws Exception
    {
        Registry.unbind(new Integer(serviceName.hashCode()));
    }


    public Object invoke(Invocation invocation)
        throws Exception
    {
        // Make sure we have the correct classloader before unmarshalling
        Thread thread = Thread.currentThread();
        ClassLoader oldCL = thread.getContextClassLoader();

        // Get the MBean this operation applies to
        ClassLoader newCL = null;
        ObjectName objectName = (ObjectName) 
            invocation.getValue("JMX_OBJECT_NAME");
        if (objectName != null) {
            // Obtain the ClassLoader associated with the MBean deployment
            newCL = (ClassLoader) 
                server.invoke(mbeanRegistry, "getValue",
                              new Object[] { objectName, CLASSLOADER },
                              new String[] { ObjectName.class.getName(),
                                             "java.lang.String" });
        }
        
        if (newCL != null && newCL != oldCL) {
            thread.setContextClassLoader(newCL);
        }

        try {
            // Set the method hash to Method mapping
            if (invocation instanceof MarshalledInvocation) {
                MarshalledInvocation mi = (MarshalledInvocation) invocation;
                mi.setMethodMap(marshalledInvocationMapping);
            }

            // Invoke the MBeanServer method via reflection
            Method method = invocation.getMethod();
            Object[] args = invocation.getArguments();
            Object value = null;
            try {
                String name = method.getName();
                Class[] sig = method.getParameterTypes();
                Method mbeanServerMethod =
                    MBeanServer.class.getMethod(name, sig);
                value = mbeanServerMethod.invoke(server, args);
            } catch(InvocationTargetException e) {
                Throwable t = e.getTargetException();
                if (t instanceof Exception) {
                    throw (Exception) t;
                } else {
                    throw new UndeclaredThrowableException(t, method.toString());
                }
            }

            return value;
        } finally {
            if (newCL != null && newCL != oldCL) {
                thread.setContextClassLoader(oldCL);
            }
        }
    }
}
InvokerAdaptorServiceMBean を構成するコンポーネントを理解しやすくするために、コードは論理ブロックに分けられ、各ブロックが動作する方法についてのコメントが付いています。

例21.2 ブロック 1

package org.jboss.jmx.connector.invoker;
public interface InvokerAdaptorServiceMBean
    extends org.jboss.system.ServiceMBean
{
    Class getExportedInterface();
    void setExportedInterface(Class exportedInterface);

    Object invoke(org.jboss.invocation.Invocation invocation)
        throws Exception;
}

package org.jboss.jmx.connector.invoker;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.jboss.invocation.Invocation;
import org.jboss.invocation.MarshalledInvocation;
import org.jboss.mx.server.ServerConstants;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.system.Registry;

public class InvokerAdaptorService
    extends ServiceMBeanSupport
    implements InvokerAdaptorServiceMBean, ServerConstants
{
    private static ObjectName mbeanRegistry;
    
    static {
        try {
            mbeanRegistry = new ObjectName(MBEAN_REGISTRY);
        } catch (Exception e) {
            throw new RuntimeException(e.toString());
        }
    }

    private Map marshalledInvocationMapping = new HashMap();
    private Class exportedInterface;

    public Class getExportedInterface()
    {
        return exportedInterface;
    }

    public void setExportedInterface(Class exportedInterface)
    {
        this.exportedInterface = exportedInterface;
    }
...
InvokerAdaptorServiceInvokerAdaptorServiceMBean Standard MBean インターフェースには単一の ExportedInterface 属性と単一の invoke(Invocation) 操作があります。
ExportedInterface
この属性によりサービスがクライアントに公開するインターフェースのタイプのカスタマイズが可能になります。これはメソッド名とシグネチャに関しては MBeanServer クラスと互換性がある必要があります。
invoke(Invocation)
この操作は、目的の MBean サービスが分離インボーカーパターンに参加するために公開しなければならない必須のエントリポイントです。この操作は InvokerAdaptorService へのアクセスを提供するように設定された分離インボーカーサービスにより呼び出されます。

例21.3 ブロック 2

    protected void startService()
        throws Exception
    {
        // Build the interface method map
        Method[] methods = exportedInterface.getMethods();
        HashMap tmpMap = new HashMap(methods.length);
        for (int m = 0; m &lt; methods.length; m ++) {
            Method method = methods[m];
            Long hash = new Long(MarshalledInvocation.calculateHash(method));
            tmpMap.put(hash, method);
        }

        marshalledInvocationMapping = Collections.unmodifiableMap(tmpMap);
        // Place our ObjectName hash into the Registry so invokers can
        // resolve it
        Registry.bind(new Integer(serviceName.hashCode()), serviceName);
    }
    protected void stopService()
        throws Exception
    {
        Registry.unbind(new Integer(serviceName.hashCode()));
    }
このコードブロックは org.jboss.invocation.MarshalledInvocation.calculateHash(Method) ユーティリティメソッドを使用して exportedInterface クラスの HashMap<Long, Method> を構築します。
java.lang.reflect.Method インスタンスはシリアル化できないため、シリアル化不可能な Invocation クラスの MarshalledInvocation バージョンは、クライアントとサーバー間の呼び出しをマーシャリングするために使用されます。MarshalledInvocation は Method インスタンスとそれらに対応するハッシュ表現とを置換します。サーバー側では、Method マッピングへのハッシュが何であるかを MarshalledInvocation に伝える必要があります。
このコードブロックは InvokerAdaptorService サービス名とそのハッシュコード表現の間のマッピングを作成します。これは分離 invoker により使用され、Invocation の目的 MBean ObjectName が何であるかを決定します。
ObjectName は作成するのに比較的高価なオブジェクトであるため、目的 MBean 名が Invocation に保存されると、その hashCode として保存されます。org.jboss.system.Registry はハッシュコードを ObjectName マッピングに保存するためにインボーカーが使用するコンストラクタのようなグローバルマップです。

例21.4 ブロック 3

    public Object invoke(Invocation invocation)
        throws Exception
    {
        // Make sure we have the correct classloader before unmarshalling
        Thread thread = Thread.currentThread();
        ClassLoader oldCL = thread.getContextClassLoader();

        // Get the MBean this operation applies to
        ClassLoader newCL = null;
        ObjectName objectName = (ObjectName) 
            invocation.getValue(&quot;JMX_OBJECT_NAME&quot;);
        if (objectName != null) {
            // Obtain the ClassLoader associated with the MBean deployment
            newCL = (ClassLoader) 
                server.invoke(mbeanRegistry, &quot;getValue&quot;,
                              new Object[] { objectName, CLASSLOADER },
                              new String[] { ObjectName.class.getName(),
                                             &quot;java.lang.String&quot; });
        }
        
        if (newCL != null &amp;&amp; newCL != oldCL) {
            thread.setContextClassLoader(newCL);
        }
このコードブロックは MBeanServer 操作が実行されている MBean の名前を取得し、MBean の SAR デプロイメントに関連付けられているクラスローダーを検索します。この情報は JBoss JMX 実装固有クラスの org.jboss.mx.server.registry.BasicMBeanRegistry で取得可能です。
通常は MBean が適切なクラスローディングコンテキストを確立することが必要です。その理由は、呼び出しと関連付けられるタイプをアンマーシャリングするために必要なクラスローダーへのアクセスを分離インボーカープロトコルレイヤが持っていない場合があるためです。

例21.5 ブロック 4

...
        try {
            // Set the method hash to Method mapping
            if (invocation instanceof MarshalledInvocation) {
                MarshalledInvocation mi = (MarshalledInvocation) invocation;
                mi.setMethodMap(marshalledInvocationMapping);
            }
...
このコードブロックでは呼び出し引数がタイプ MarshalledInvocation である場合 ExposedInterface クラスメソッドのハッシュをメソッドマッピングにインストールします。例21.3「ブロック 2」 で計算されるメソッドマッピングはここで使用されます。
第 2 のマッピングは ExposedInterface メソッドから MBeanServer クラスの一致するメソッドへ実行されます。InvokerServiceAdaptor は任意のインターフェースを可能にするため MBeanServer クラスからのExposedInterface を切り離します。これが必要なのは標準 java.lang.reflect.Proxy クラスはインターフェースをプロキシすることのみ可能であるためです。また、MBeanServer メソッドのサブセットを公開し、java.rmi.RemoteException などのトランスポート固有の例外を ExposedInterface メソッドシグネチャに追加することだけが可能になります。

例21.6 ブロック 5

...
            // Invoke the MBeanServer method via reflection
            Method method = invocation.getMethod();
            Object[] args = invocation.getArguments();
            Object value = null;
            try {
                String name = method.getName();
                Class[] sig = method.getParameterTypes();
                Method mbeanServerMethod =
                    MBeanServer.class.getMethod(name, sig);
                value = mbeanServerMethod.invoke(server, args);
            } catch(InvocationTargetException e) {
                Throwable t = e.getTargetException();
                if (t instanceof Exception) {
                    throw (Exception) t;
                } else {
                    throw new UndeclaredThrowableException(t, method.toString());
                }
            }

            return value;
        } finally {
            if (newCL != null &amp;&amp; newCL != oldCL) {
                thread.setContextClassLoader(oldCL);
            }
        }
    }
}
このコードブロックは MBeanServer メソッド呼び出しをそれがデプロイされる InvokerAdaptorService MBeanServer インスタンスにディスパッチします。サーバーのインスタンス変数は ServiceMBeanSupport スーパークラスから引き継がれます。
呼び出しにより投げられたすべての宣言された例外のアンラップを含め、反射型呼び出しから生じる例外は処理されます。MBean コードは MBeanServer メソッド呼び出しが成功した結果の戻りにより完了します。

注記

InvokerAdaptorService MBean はトランスポート固有の詳細は直接扱いません。Method マッピングへのメソッドハッシュの計算がありますが、これはトランスポート独自の詳細です。
ここで、「RMI を使用した JMX への接続」で見たように RMI/JRMP により同じ org.jboss.jmx.adaptor.rmi.RMIAdaptor インターフェースを公開するために InvokerAdaptorService がどのように使用されるかを見てみましょう。
jmx-invoker-adaptor-service.sar デプロイメントのデフォルト設定にあるプロキシファクトリと InvokerAdaptorService 設定を表示することから始めます。例21.7「デフォルトの jmx-invoker-adaptor-server.sar デプロイメント記述子」 はこのデプロイメントに対する jboss-service.xml 記述子を示しています。

例21.7 デフォルトの jmx-invoker-adaptor-server.sar デプロイメント記述子

<server>
    <!-- The JRMP invoker proxy configuration for the InvokerAdaptorService -->
    <mbean code="org.jboss.invocation.jrmp.server.JRMPProxyFactory"
           name="jboss.jmx:type=adaptor,name=Invoker,protocol=jrmp,service=proxyFactory">
        <!-- Use the standard JRMPInvoker from conf/jboss-service.xml -->
        <attribute name="InvokerName">jboss:service=invoker,type=jrmp</attribute>
        <!-- The target MBean is the InvokerAdaptorService configured below -->
        <attribute name="TargetName">jboss.jmx:type=adaptor,name=Invoker</attribute>
        <!-- Where to bind the RMIAdaptor proxy -->
        <attribute name="JndiName">jmx/invoker/RMIAdaptor</attribute>
        <!-- The RMI compatible MBeanServer interface -->
        <attribute name="ExportedInterface">org.jboss.jmx.adaptor.rmi.RMIAdaptor</attribute>
        <attribute name="ClientInterceptors">
            <iterceptors>
                <interceptor>org.jboss.proxy.ClientMethodInterceptor</interceptor>
                <interceptor>
                    org.jboss.jmx.connector.invoker.client.InvokerAdaptorClientInterceptor 
                </interceptor>
                <interceptor>org.jboss.invocation.InvokerInterceptor</interceptor>
            </iterceptors>
        </attribute>
        <depends>jboss:service=invoker,type=jrmp</depends>
    </mbean> 
    <!-- This is the service that handles the RMIAdaptor invocations by routing
         them to the MBeanServer the service is deployed under. -->
    <mbean code="org.jboss.jmx.connector.invoker.InvokerAdaptorService" 
           name="jboss.jmx:type=adaptor,name=Invoker">
        <attribute name="ExportedInterface">org.jboss.jmx.adaptor.rmi.RMIAdaptor</attribute>
    </mbean>
</server>
最初の MBean org.jboss.invocation.jrmp.server.JRMPProxyFactory は RMI/JRMP プロトコルに対しプロキシを作成するプロキシファクトリ MBean サービスです。例21.7「デフォルトの jmx-invoker-adaptor-server.sar デプロイメント記述子」 で示されているようにこのサービスの設定が示していることは次のとおりです。JRMPInvoker が 分離インボーカーとして使用され、InvokerAdaptorService は要求が転送される目的の Mbean であること、プロキシが RMIAdaptor インターフェースに公開され、プロキシが名前 jmx/invoker/RMIAdaptor でJNDI にバインドされ、プロキシが 3 つのインターセプタ ClientMethodInterceptorInvokerAdaptorClientInterceptorInvokerInterceptor を含むということです。InvokerAdaptorService の設定はサービスが公開している RMIAdaptor インターフェースを単に設定することです。
RMI/JRMP による InvokerAdaptorService を公開するための設定の最後は分離インボーカーです。使用する分離インボーカーはホームとリモート呼び出しに EJB コンテナが使用する標準 RMI/JRMP インボーカーであり、これは conf/jboss-service.xml 記述子で設定された org.jboss.invocation.jrmp.server.JRMPInvoker MBean サービスです。同じサービスインスタンスを使用できることはインボーカーの分離という性質を強調しています。プロキシが公開するインターフェースまたはプロキシが活用するサービスに関わらず、JRMPInvoker は単にすべての RMI/JRMP プロキシに対して RMI/JRMP エンドポイントとして働きます。