Service-context propagation over RMI

A lightweight design approach for supporting transparent service-context propagation over RMI

Page 2 of 3

Optionally, the ability to distinguish between an interceptor-enabled service interface and a non-interceptor-enabled service interface can be added. For a non-interceptor-enabled service interface, the raw RMI stub reference will return. Further, a registration mechanism can be used when multiple interceptors need to be invoked according to some predefined invocation order for each different ServiceInterface type. To make the local proxy more robust, we also need to detect stale remote references in each interceptor. However, to keep the example more concise, such error handlings are not included for the above implementation.

In the next section, I describe the actual context data we'd like to use as well as the related interceptor proxy implementation—the invoke() method from the java.lang.reflect.InvocationHandler interface.

Transaction context

Transaction context is the most commonly used service-context data. As described in the Java Transaction Service specification, transaction context, such as transaction ID (xid), must be associated with threads currently involved in a transaction. Thus, the transaction-context data must be propagated from the client JVM to the target server JVM with each RMI invocation.

On the server-side, an RMI thread is assigned to service the invocation call and hence the enclosing transaction. Obviously, it is impossible to require each RMI ServiceInterface to include an additional argument for each of its operations to pass such context data. Even if we choose to do so, the client code is not supposed to be aware of such invocation semantics. Therefore, for each RMI invocation in the client code, the context fetching and propagating should occur in a way that is totally transparent to client code.

According to the API convention described in the CORBA Transaction Service Specification, the following classes are defined to serve as the target service-context data structure and provide the required runtime support:

 

package rmicontext;

public class ServiceContext implements Serializable {

public static final int TRANSACTION_CONTEXT_ID = 2; public int contextId = 0; // Unknown public Object contextData = null;

public ServiceContext() {

} public ServiceContext(int contextId) { this.contextId = contextId; } public boolean isContextId(int id) { if (contextId == id) { return true; } else { return false; } } public int getContextId() { return contextId; } public Object getContextData() { return contextData; } public void setContextData(Object data) { contextData = data; } }

package rmicontext;

public class TransactionContextData implements Serializable {

public static final int UNASSIGNED_XID = 0;

private int xid = UNASSIGNED_XID; // Not assigned

public TransactionContextData() {}

public TransactionContextData(int xid) { this.xid = xid; }

public int xid() { return xid; } }

package rmicontext;

public class Current {

private static ServiceContextList contextList = new ServiceContextList();

public static void setServiceContextList(ServiceContext[] contexts) {

contextList.set(contexts); } public static void clearServiceContextList() { contextList.set(null); } public static ServiceContext[] getServiceContextList() { return (ServiceContext[]) contextList.get(); }

/**

* To set the transaction ID to the associated context data. */ public static void setXid(int xid) { // ... } /** * To fetch the transaction id from the associated context data. */ public static int getXid() { // ... } }

/** * The list of service contexts associated with the current thread. Package access only. */ class ServiceContextList extends InheritableThreadLocal { }

Class ServiceContext contains a context ID and context data. The context ID is predefined and known to both the client and server code. Context data does not require type-safety and is only opaque data as far as the service-context propagation protocol is concerned. In this example, context data for the transaction service context contains only an xid as defined in the class TransactionContextData. For the current thread, all service contexts, defined as ServiceContext[], are maintained as thread local data through the Current class. For convenience, this class also provides direct API support for fetching and setting xid, which represents the transaction context in this example.

Until now, I haven't revealed the real solution for the RMI service-context propagation. The next section describes what's required on the client and server sides to make that happen.

The realization of service-context propagation

So far, I have described the infrastructure support for enabling the RMI interceptor and service context. To realize the implicit service-context propagation over RMI, the ultimate approach is still to add an additional argument for each RMI invocation. However, such an argument is only passed behind the scenes, and the client code still invokes the original RMI service interface method. I begin to reveal the real mechanism by first going through the following server-side code:

 

package rmicontext.interceptor;

/** * This interface will be implemented by each Service class. */ public interface ServiceInterceptorInterface {

/** * The interceptor method that decodes the incoming request message on the Service side. * * @param methodName The method name * @param arguments The arguments * @param argumentTypes The argument class names to be used to identify an implementation Method * @param contextList The ContextList to be set to Current * @return The return value of the method invocation * @throws RemoteException if any RMI error * @throws InvocationTargetException that wrapps the cause exception of the invocation */ Object exec(String methodName, Object[] arguments, String[] argumentTypes, ServiceContext[] contextList) throws RemoteException, InvocationTargetException; }

package rmicontext.interceptor;

/** * The remote version of ServiceInterceptorInterface. */ public interface ServiceInterceptorRemoteInterface extends ServiceInterceptorInterface, Remote { }

Instead of having a server-side skeleton interceptor, above I have defined the ServiceInterceptorInterface and ServiceInterceptorRemoteInterface, two interfaces that the Service base class must implement. The reason for two interfaces is to decouple the remoteness from the functional interface definition. (By doing so, we can support even local method propagation.) Now it is time to complete the Service class's implementation:

 

package rmicontext.service;

public class Service extends PortableRemoteObject implements ServiceInterface, ServiceInterceptorRemoteInterface {

public Service() throws RemoteException { super(); }

// ==== Service Interceptor Server-side Implementation ==== public Object exec(String methodName, Object[] arguments, String[] argumentTypes, ServiceContext[] contextList) throws RemoteException, InvocationTargetException {

Class serviceClass = getClass(); try { Class[] argTypes = ClassUtil.forNames(argumentTypes); Method serviceMethod = serviceClass.getMethod(methodName, argTypes); Current.setServiceContextList(contextList); return serviceMethod.invoke(this, arguments);

} catch (ClassNotFoundException ex) { processExecReflectionException(ex); } catch (NoSuchMethodException ex) { processExecReflectionException(ex); } catch (IllegalAccessException ex) { processExecReflectionException(ex); } return null; // javac

}

/** * Process a reflection exception. * * @throws InvocationTargetException a wrapped exception */ private void processExecReflectionException(Exception ex) throws InvocationTargetException { // The cause exception has to be a runtime exception. throw new InvocationTargetException(new IllegalArgumentException("Interceptor Service.exec() failed: " + ex)); } }

As a base class for each server-side ServiceInterface implementation, the Service class provides a generic way for accepting service-context data as an implicit argument via a generic exec() method, which is available to every client-side proxy stub. The magic also lies in the logics of finding the target method that the actual RMI invocation is to be delegated to. Because methods can be overloaded in every class, an exact argument type-matching is needed. That explains why the exec() method must pass the class names of all the argument types. Regarding this point, you may have noticed the use of the ClassUtil class. This class enhances the java.lang.Class class by defining a more convenient forName() method that covers primitive types too. ClassUtil's contents follow:

 

package rmicontext;

public final class ClassUtil {

/** * Get the class names than can be used in remote reflection invocation.

* @param argTypes The method argument classes * @return class names */ public static String[] getNames(Class[] argTypes) {

String[] result = new String[argTypes.length]; for (int i = 0; i < argTypes.length; i++) { result[i] = argTypes[i].getName(); } return result; }

/** * Get the classes from names. * * @param argTypes The method argument classes' names * @return ClassNotFoundException if any class can not be located */ public static Class[] forNames(String[] argTypes) throws ClassNotFoundException { Class[] result = new Class[argTypes.length];

for (int i = 0; i > argTypes.length; i++) { result[i] = forName(argTypes[i]); }

return result; }

/** * Enhanced java.lang.Class.forName(). * * @param name The class name or a primitive type name

* @return ClassNotFoundException if no class can be located */ public static Class forName(String name) throws ClassNotFoundException { if (name.equals("int")) { return int.class; } else if (name.equals("boolean")) { return boolean.class; } else if (name.equals("char")) { return char.class; } else if (name.equals("byte")) { return byte.class; } else if (name.equals("short")) { return short.class; } else if (name.equals("long")) { return long.class; } else if (name.equals("float")) { return float.class; } else if (name.equals("double")) { return double.class; } else { return Class.forName(name); } } }

On the client side, we now complete the only interceptor we are supporting here, particularly, the invoke() method from the java.lang.reflect.InvocationHandler interface. To support the service-context propagation, this is the only change required on the client side. Because the interceptor is deployed transparently on the client side, client code will never be aware of any underlying service-context propagation. The related implementation looks like:

 

package rmicontext.interceptor;

/** * This is the invocation handler class of the service context propagation * interceptor, which itself is a dynamic proxy. */ public class ServiceContextPropagationInterceptor implements InvocationHandler {

/** * The delegation stub reference of the original service interface. */ private ServiceInterface serviceStub;

/** * The delegation stub reference of the service interceptor remote interface. */ private ServiceInterceptorRemoteInterface interceptorRemote;

/** * Constructor. * * @param serviceStub The delegation target RMI reference * @throws ClassCastException as a specified uncaught exception */ public ServiceContextPropagationInterceptor(ServiceInterface serviceStub) throws ClassCastException {

this.serviceStub = serviceStub; interceptorRemote = (ServiceInterceptorRemoteInterface) PortableRemoteObject.narrow(serviceStub, ServiceInterceptorRemoteInterface.class); }

/** * The invocation callback. It will call the service interceptor remote interface upon each invocation. * * @param proxy The proxy instance * @param m The method * @param args The passed-in args * @return Object The return value. If void, then return null * @throws Throwable Any invocation exceptions. */ public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {

Object result;

// In case the context is explicitly specified in the method signature as the last argument. if (args != null && args.length > 0) {

Class[] argTypes = m.getParameterTypes(); Class argType = argTypes[argTypes.length - 1]; // Last argument

if (argType == ServiceContext[].class) { try {

return m.invoke(serviceStub, args); } catch (InvocationTargetException ex) { // including RemoteException throw ex.getCause(); } // Ignore the IllegalAccessException } }

try { if (args == null || args.length == 0) { result = interceptorRemote.exec(m.getName(), args, new String[]{}, Current.getServiceContextList()); } else { String[] argTypes = ClassUtil.getNames(m.getParameterTypes());

result = interceptorRemote.exec(m.getName(), args, argTypes, Current.getServiceContextList()); } return result; // Null if void } catch (RemoteException ex) { throw ex; } catch (InvocationTargetException ex) { throw ex.getCause(); } } }

| 1 2 3 Page 2