21.6. Remote Access to Services, Detached Invokers

In addition to the MBean services notion that allows for the ability to integrate arbitrary functionality, JBoss also has a detached invoker concept that allows MBean services to expose functional interfaces via arbitrary protocols for remote access by clients. The notion of a detached invoker is that remoting and the protocol by which a service is accessed is a functional aspect or service independent of the component. Therefore, you can make a naming service available for use via RMI/JRMP, RMI/HTTP, RMI/SOAP, or any arbitrary custom transport.
The discussion of the detached invoker architecture will begin with an overview of the components involved. The main components in the detached invoker architecture are shown in Figure 21.1, “The main components in the detached invoker architecture”.
The main components in the detached invoker architecture

Figure 21.1. The main components in the detached invoker architecture

On the client side, there exists a client proxy which exposes the interface(s) of the MBean service. This is the same smart, compile-less dynamic proxy that is used for EJB home and remote interfaces. The only difference between the proxy for an arbitrary service and the EJB is the set of interfaces exposed as well as the client side interceptors found inside the proxy. The client interceptors are represented by the rectangles found inside of the client proxy. An interceptor is an assembly line type of pattern that allows for transformation of a method invocation and/or return values. A client obtains a proxy through some lookup mechanism, typically JNDI. Although RMI is indicated in Figure 21.1, “The main components in the detached invoker architecture”, the only real requirement on the exposed interface and its types is that they are serializable between the client server over JNDI as well as the transport layer.
The choice of the transport layer is determined by the last interceptor in the client proxy, which is referred to as the Invoker Interceptor in Figure 21.1, “The main components in the detached invoker architecture”. The invoker interceptor contains a reference to the transport specific stub of the server side Detached Invoker MBean service. The invoker interceptor also handles the optimization of calls that occur within the same VM as the target MBean. When the invoker interceptor detects that this is the case the call is passed to a call-by-reference invoker that simply passes the invocation along to the target MBean.
The detached invoker service is responsible for making a generic invoke operation available via the transport the detached invoker handles. The Invoker interface illustrates the generic invoke operation.
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;
}
The Invoker interface extends Remote to be compatible with RMI, but this does not mean that an invoker must expose an RMI service stub. The detached invoker service acts as a transport gateway that accepts invocations represented as the org.jboss.invocation.Invocation object over its specific transport. The invoker service unmarshals the invocation, forwards the invocation onto the destination MBean service represented by the Target MBean in Figure 21.1, “The main components in the detached invoker architecture”, and marshals the return value or exception resulting from the forwarded call back to the client.
The Invocation object is just a representation of a method invocation context. This includes the target MBean name, the method, the method arguments, a context of information associated with the proxy by the proxy factory, and an arbitrary map of data associated with the invocation by the client proxy interceptors.
The configuration of the client proxy is done by the server side proxy factory MBean service, indicated by the Proxy Factory component in Figure 21.1, “The main components in the detached invoker architecture”. The proxy factory performs the following tasks:
  • Create a dynamic proxy that implements the interface the target MBean wishes to expose.
  • Associate the client proxy interceptors with the dynamic proxy handler.
  • Associate the invocation context with the dynamic proxy. This includes the target MBean, detached invoker stub and the proxy JNDI name.
  • Make the proxy available to clients by binding the proxy into JNDI.
The last component in Figure 21.1, “The main components in the detached invoker architecture” is the Target MBean service that wishes to expose an interface for invocations to remote clients. The steps required for an MBean service to be accessible through a given interface are:
  • Define a JMX operation matching the signature: public Object invoke(org.jboss.invocation.Invocation) throws Exception
  • Create a HashMap<Long, Method> mapping from the exposed interface java.lang.reflect.Methods to the long hash representation using the org.jboss.invocation.MarshalledInvocation.calculateHash method.
  • Implement the invoke(Invocation) JMX operation and use the interface method hash mapping to transform from the long hash representation of the invoked method to the java.lang.reflect.Method of the exposed interface. Reflection is used to perform the actual invocation on the object associated with the MBean service that actually implements the exposed interface.

21.6.1. A Detached Invoker Example, the MBeanServer Invoker Adaptor Service

This section presents the org.jboss.jmx.connector.invoker.InvokerAdaptorService and its configuration for access via RMI/JRMP as an example of the steps required to provide remote access to an MBean service.

Example 21.1. The InvokerAdaptorService MBean

The InvokerAdaptorService is a simple MBean service that exists to fulfill the target MBean role in the detached invoker pattern.
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);
        }
    }
}
}
To help understand the components that make up the InvokerAdaptorServiceMBean, the code has been split into logical blocks, with commentary about how each block operates.

Example 21.2. Block One

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;
}
...
The InvokerAdaptorServiceMBean Standard MBean interface of the InvokerAdaptorService has a single ExportedInterface attribute and a single invoke(Invocation) operation.
ExportedInterface
The attribute allows customization of the type of interface the service exposes to clients. This must be compatible with the MBeanServer class in terms of method name and signature.
invoke(Invocation)
The operation is the required entry point that target MBean services must expose to participate in the detached invoker pattern. This operation is invoked by the detached invoker services that have been configured to provide access to the InvokerAdaptorService.

Example 21.3. Block Two

    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()));
}
This code block builds the HashMap<Long, Method> of the exportedInterface Class using the org.jboss.invocation.MarshalledInvocation.calculateHash(Method) utility method.
Because java.lang.reflect.Method instances are not serializable, a MarshalledInvocation version of the non-serializable Invocation class is used to marshal the invocation between the client and server. The MarshalledInvocation replaces the Method instances with their corresponding hash representation. On the server side, the MarshalledInvocation must be told what the hash to Method mapping is.
This code block creates a mapping between the InvokerAdaptorService service name and its hash code representation. This is used by detached invokers to determine what the target MBean ObjectName of an Invocation is.
When the target MBean name is stored in the Invocation, its store as its hashCode because ObjectNames are relatively expensive objects to create. The org.jboss.system.Registry is a global map like construct that invokers use to store the hash code to ObjectName mappings in.

Example 21.4. Block Three

    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);
    }
This code block obtains the name of the MBean on which the MBeanServer operation is being performed, and then looks up the class loader associated with the MBean's SAR deployment. This information is available via the org.jboss.mx.server.registry.BasicMBeanRegistry, a JBoss JMX implementation-specific class.
It is generally necessary for an MBean to establish the correct class loading context because the detached invoker protocol layer may not have access to the class loaders needed to unmarshal the types associated with an invocation.

Example 21.5. Block Four

...
    try {
        // Set the method hash to Method mapping
        if (invocation instanceof MarshalledInvocation) {
            MarshalledInvocation mi = (MarshalledInvocation) invocation;
            mi.setMethodMap(marshalledInvocationMapping);
        }
...
This code block installs the ExposedInterface class method hash to method mapping if the invocation argument is of type MarshalledInvocation. The method mapping calculated in Example 21.3, “Block Two”is used here.
A second mapping is performed from the ExposedInterface method to the matching method of the MBeanServer class. The InvokerServiceAdaptor decouples the ExposedInterface from the MBeanServer class in that it allows an arbitrary interface. This is required because the standard java.lang.reflect.Proxy class can only proxy interfaces. It also allows you to only expose a subset of the MBeanServer methods and add transport specific exceptions such as java.rmi.RemoteException to the ExposedInterface method signatures.

Example 21.6. Block Five

...
        // 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);
        }
    }
}
}
The code block dispatches the MBeanServer method invocation to the InvokerAdaptorService MBeanServer instance to which the was deployed. The server instance variable is inherited from the ServiceMBeanSupport superclass.
Any exceptions that result from the reflective invocation are handled, including unwrapping any declared exceptions thrown by the invocation. The MBean code completes with the return of the successful MBeanServer method invocation result.

Note

The InvokerAdaptorService MBean does not deal directly with any transport specific details. There is the calculation of the method hash to Method mapping, but this is a transport independent detail.
Now take a look at how the InvokerAdaptorService may be used to expose the same org.jboss.jmx.adaptor.rmi.RMIAdaptor interface via RMI/JRMP as seen in Connecting to JMX Using RMI.
We start by presenting the proxy factory and InvokerAdaptorService configurations found in the default setup in the jmx-invoker-adaptor-service.sar deployment. Example 21.7, “Default jmx-invoker-adaptor-server.sar deployment descriptor” shows the jboss-service.xml descriptor for this deployment.

Example 21.7. Default jmx-invoker-adaptor-server.sar deployment descriptor

<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>
The first MBean, org.jboss.invocation.jrmp.server.JRMPProxyFactory, is the proxy factory MBean service that creates proxies for the RMI/JRMP protocol. The configuration of this service as shown in Example 21.7, “Default jmx-invoker-adaptor-server.sar deployment descriptor” states that the JRMPInvoker will be used as the detached invoker, the InvokerAdaptorService is the target mbean to which requests will be forwarded, that the proxy will expose the RMIAdaptor interface, the proxy will be bound into JNDI under the name jmx/invoker/RMIAdaptor, and the proxy will contain 3 interceptors: ClientMethodInterceptor, InvokerAdaptorClientInterceptor, InvokerInterceptor. The configuration of the InvokerAdaptorService simply sets the RMIAdaptor interface that the service is exposing.
The last piece of the configuration for exposing the InvokerAdaptorService via RMI/JRMP is the detached invoker. The detached invoker we will use is the standard RMI/JRMP invoker used by the EJB containers for home and remote invocations, and this is the org.jboss.invocation.jrmp.server.JRMPInvoker MBean service configured in the conf/jboss-service.xml descriptor. That we can use the same service instance emphasizes the detached nature of the invokers. The JRMPInvoker simply acts as the RMI/JRMP endpoint for all RMI/JRMP proxies regardless of the interface(s) the proxies expose or the service the proxies utilize.