Service-context propagation over RMI

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

CORBA's service context provides an efficient and elegant design and implementation approach for building distributed systems. Java RMI (Remote Method Invocation) can't easily support transparent service-context propagation without instrumenting the underlying protocol. This article describes a generic lightweight solution for supporting transparent and protocol-independent service-context propagation over RMI. Reflection-based techniques are used to emulate what's normally seen in protocol-specific service-context implementations.

This article introduces you to a real-world solution and the related distributed-computing design concept, as well as Java reflection techniques. We start with an overview of the CORBA object request broker (ORB) interceptor and the service-context design architecture. Then a concrete implementation example describes the actual solution and demonstrates how RMI invocation is actually massaged to propagate service-context data, such as transaction context, which is usually offered through the IIOP (Internet Inter-ORB Protocol) layer. Lastly, performance considerations are discussed.

Interceptor and service context in CORBA

In the CORBA architecture, the invocation interceptor plays an important role in the function provided by the ORB runtime. Generally speaking, four interception points are available through the ORB runtime. As shown in Figure 1, these interception points are for:

  • Out-bound request messages from the client process
  • In-bound request messages to the server process
  • Out-bound response messages from the server process
  • In-bound response messages to the client process

The so-called portable interceptor can support both a request-level interceptor (pre-marshaling) and a message-level interceptor (post-marshaling). More specific details can be found in the CORBA specification documents.

Figure 1. ORB invocation interceptors.

Interceptors provide a powerful and flexible design support to both ORB vendors and application builders for constructing highly distributed applications. Value-adding functions can be transparently added and enabled at the protocol, ORB, or application layers without complicating standardized application-level APIs. Examples include invocation monitoring, logging, and message routing. In some sense, we are looking for a kind of RMI-level AOP (aspect-oriented programming) support.

Among the many uses of interceptors, propagating service-context data is one of the most important. Effectively, service-context propagation provides a way to extend the ORB runtime and IIOP protocol without affecting applications built on top of the ORB, such as IDL (interface definition language) definitions. CORBA services, such as transaction and security services, standardize and publish the structure of their specific service-context data as IDL data types, together with the unique context identifiers (context_id).

Simply put, service-context data is information that the ORB runtime (RMI runtime, for this article's purposes) manages to support infrastructure-level services that the runtime provides to hosted applications. The information usually must be piggybacked on each invocation message between the client process and the server process. ORB runtime and related infrastructure-level services are responsible for sending, retrieving, interpreting, and processing such context data and delivering it to the application layer whenever necessary. Service-context data is passed with each request and reply message with no application interface-level exposure, such as at the IDL layer.

Nevertheless, it is not fair to ask RMI to directly support such capabilities as it is only a basic remote method invocation primitive, while CORBA ORB is at a layer close to what the J2EE EJB (Enterprise JavaBean) container offers. In the CORBA specification, service context is directly supported at the IIOP level (GIOP, or General Inter-Orb Protocol) and integrated with the ORB runtime. However, for RMI/IIOP, it is not easy for applications to utilize the underlying IIOP service-context support, even when the protocol layer does have such support. At the same time, such support is not available when RMI/JRMP (Java Remote Method Protocol) is used. As a result, for RMI-based distributed applications that do not use, or do not have to use, an ORB or EJB container environment, the lack of such capabilities limits the available design choices, especially when existing applications must be extended to support new infrastructure-level functions. Modifying existing RMI interfaces often proves undesirable due to the dependencies between components and the huge impact to client-side applications. The observation of this RMI limitation leads to the generic solution that I describe in this article.

The high-level picture

The solution is based on Java reflection techniques and some common methods for implementing interceptors. More importantly, it defines an architecture that can be easily integrated into any RMI-based distributed application design. I demonstrate the solution through an example implementation that supports the transparent passing of transaction-context data, such as a transaction ID (xid), with RMI. The example's source code is available for download from Resources. The solution contains the following three components:

  1. RMI remote interface naming-function encapsulation and interceptor plug-in (rmicontex.interceptor.*)
  2. Service-context propagation mechanism and server-side interface support (rmicontex.service.*)
  3. Service-context data structure and transaction-context propagation support (rmicontex.*)

The components' corresponding Java class packages are shown in Figure 2.

Figure 2. The component view of packages

The example is not meant to be used as a whole package solution; rather, the implementation demonstrates the underlying design approach. The implementation assumes that RMI/IIOP is used. However, it by no means implies that this solution is only for RMI/IIOP. In fact, either RMI/JRMP or RMI/IIOP can be used as the underlying RMI environments, or even a hybrid of the two environments if the naming service supports both.

Naming-function encapsulation

To implement our solution, first we encapsulate the naming function that provides the RMI remote interface lookup, allowing interceptors to be transparently plugged in. Such an encapsulation is always desirable and can always be found in most RMI-based applications. The underlying naming resolution mechanism is not a concern here; it can be anything that supports JNDI (Java Naming and Directory Interface). In this example, to make the code more illustrative, we assume all server-side remote RMI interfaces inherit from a mark remote interface ServiceInterface, which itself inherits from the Java RMI Remote interface. Figure 3 shows the class diagram, which is followed by code snippets that I will describe further:

Figure 3. Class diagram of ServiceInterface and ServiceManager

package rmicontext.service;

public interface ServiceInterface extends Remote { }

package rmicontext.service;

public class Service extends PortableRemoteObject implements ServiceInterface, ServiceInterceptorRemoteInterface { .... }

package rmicontext.service;

public interface ServiceManagerInterface { public ServiceInterface getServiceInterface(String serviceInterfaceClassName); }

package rmicontext.service;

public class ServiceManager implements ServiceManagerInterface {

/** * Gets a reference to a service interface. * * @param serviceInterfaceClassName The full class name of the requested interface * @return selected service interface */ public ServiceInterface getServiceInterface(String serviceInterfaceClassName) { // The actual naming lookup is skipped here ...

} }

The Service serves as the base class for any server-side RMI remote interface implementation. No real code is needed at the moment. For simplicity, we just use the RMI remote interface Class name as the key for the interface naming lookup. The naming lookup is encapsulated through the class ServiceManager, which implements the interface ServiceManagerInterface as the new encapsulated naming API.

In the next section, you find out how the interceptor is plugged in. A simple interface-caching implementation is also included to complete the class ServiceManager.

RMI invocation interceptor

To enable the invocation interceptor, the original RMI stub reference acquired from the RMI naming service must be wrapped by a local proxy. To provide a generic implementation, such a proxy is realized using a Java dynamic proxy API. In the runtime, a proxy instance is created; it implements the same ServiceInterface RMI interface as the wrapped stub reference. Any invocation will be delegated to the stub eventually after first being processed by the interceptor. A simple implementation of an RMI interceptor factory follows the class diagram shown in Figure 4.

Figure 4. RMI interceptor factory

package rmicontext.interceptor;

public interface ServiceInterfaceInterceptorFactoryInterface { ServiceInterface newInterceptor(ServiceInterface serviceStub, Class serviceInterfaceClass) throws Exception; }

package rmicontext.interceptor;

public class ServiceInterfaceInterceptorFactory implements ServiceInterfaceInterceptorFactoryInterface {

public ServiceInterface newInterceptor(ServiceInterface serviceStub, Class serviceInterfaceClass) throws Exception {

ServiceInterface interceptor = (ServiceInterface) Proxy.newProxyInstance(serviceInterfaceClass.getClassLoader(), new Class[]{serviceInterfaceClass}, new ServiceContextPropagationInterceptor(serviceStub)); // ClassCastException

return interceptor; } }

package rmicontext.interceptor;

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

public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { // Skip it for now ... } }

I have simplified the above code to focus more on the underlying design. Here, only one type of interceptor is created, and it is implemented as the ServiceContextPropagationInterceptor class. This interceptor is responsible for retrieving and passing all the service-context data available under the current invocation scope. More detail will be covered later. The interceptor factory is used by the naming-function encapsulation described in the previous section.

To complete the ServiceManager class, a simple interface proxy cache is implemented:


package rmicontext.service;

public class ServiceManager implements ServiceManagerInterface {

/** * The interceptor stub reference cache. * <br><br> * The key is the specific serviceInterface sub-class and the value is the interceptor stub reference. */ private transient HashMap serviceInterfaceInterceptorMap = new HashMap();

/** * Gets a reference to a service interface. * * @param serviceInterfaceClassName The full class name of the requested interface * @return selected service interface */ public ServiceInterface getServiceInterface(String serviceInterfaceClassName) {

// The actual naming lookup is skipped here. ServiceInterface serviceInterface = ...;

synchronized (serviceInterfaceInterceptorMap) {

if (serviceInterfaceInterceptorMap.containsKey(serviceInterfaceClassName)) { WeakReference ref = (WeakReference) serviceInterfaceInterceptorMap.get(serviceInterfaceClassName); if (ref.get() != null) { return (ServiceInterface) ref.get(); } } try { Class serviceInterfaceClass = Class.forName(serviceInterfaceClassName);

ServiceInterface serviceStub = (ServiceInterface) PortableRemoteObject.narrow(serviceInterface, serviceInterfaceClass);

ServiceInterfaceInterceptorFactoryInterface factory = ServiceInterfaceInterceptorFactory.getInstance(); ServiceInterface serviceInterceptor = factory.newInterceptor(serviceStub, serviceInterfaceClass);

WeakReference ref = new WeakReference(serviceInterceptor); serviceInterfaceInterceptorMap.put(serviceInterfaceClassName, ref);

return serviceInterceptor; } catch (Exception ex) { return serviceInterface; // no interceptor } } } }

1 2 3 Page 1
Page 1 of 3
How to choose a low-code development platform