Service-context propagation over RMI: Implementation follow-up

A protocol-independent lightweight framework for providing service-context propagation and interceptor support over RMI

My article "Service-Context Propagation over RMI" (JavaWorld, January 2005) introduces service-context and interceptor concepts common in CORBA-based solutions. The article also defines a generic design approach for supporting such concepts in Java Remote Method Invocation.

As a follow-up, this article describes a concrete implementation of a lightweight and protocol-independent framework for providing generic interceptor support and service-context propagation over RMI. The goal of this article is to highlight important design approaches and answer questions that readers may have after having read the previous article. More importantly, the implementation illustrates what makes a framework and demonstrates the value for having a protocol-independent solution. The source code for the implementation is available on SourceForge.net. The SourceForge project is an ongoing effort. The first release serves as this article's main reference.

The design layout

The most important part of this article's framework is the support of an interceptor, which is required on both the client and server. On the client-side, the interceptor is invoked by a client-side RMI stub proxy, which is a dynamic proxy that I will discuss later. Figure 1 gives an architectural view of the target RMI interceptor design.

Figure 1. RMI interceptor architecture

As shown in Figure 1, upon each RMI invocation, interceptors control the flow of each request and reply between the client stub and server skeleton. In general, the interceptor is stateless and reentrant. It is deployed per JVM (classloader) and reused by every stub and skeleton, which are simply logical components here and may or may not be visible to application code.

Figure 1 ignores the exception flow, and the actual interception flow is in fact more complicated, as illustrated in Figures 2 and 3, which describe what occurs on the client and server, respectively.

Figure 2. Client-side interceptor control flow
Figure 3. Server-side interceptor control flow

For each RMI invocation, multiple interceptors are invoked in a determined order. On the client, the interception point sendRequest() serves as a starting point and receiveReply() serves as an end point. An end point is invoked only if the corresponding starting point of the same interceptor has run to completion. An exception may be injected from both the wire and the interceptor itself. The same rule applies to the server: the starting point is receiveRequestServiceContexts instead. For all server-side interceptors, receiveRequestServiceContexts interception points run first.

An interceptor's design and API follow the latest CORBA Portable Interceptors specification wherever applicable to Java RMI. Figure 4 defines the interface RMIInterceptor.

Figure 4. RMIInterceptor class diagram

In this extended RMI framework, interceptors are managed as a stack flow. The relative order of interceptors is insignificant, and interceptors should be designed so they don't depend on each other and can function in isolation. However, once deployed, the invocation order of multiple deployed interceptors is determined. Between the client and the server, the order may differ. Also, some interceptors may apply only to the client or only to the server.

As stated earlier, an interceptor is stateless, and each interception point is represented essentially as a reentrant method of the RMIInterceptor interface. The method accepts all the RMI invocation contexts via the parameter class RequestInfo. This parameter behaves as a pass-by-value argument. Each interception point sets or manipulates the data inside and passes the resulting RequestInfo to the next interception point, if any, throughout the whole invocation flow. Figure 5 defines RequestInfo's structure.

Figure 5. RequestInfo class diagram

Note: Further detail on service-context propagation can be found in the source code's Javadoc and demo package, and my previous article (see Resources for links).

Now let's examine the project's Java class packages, which are summarized as follows:

  • Standard RMI Interceptor API (net.sf.extrm.*)
  • Service-context support (net.sf.extrm.context.*)
  • Naming interface and dynamic stub proxy factory (net.sf.extrm.naming.*)
  • Server-side RMI interceptor support implementation (net.sf.extrm.server.*)
  • Common-purpose interceptor classes (net.sf.extrm.interceptors.*)
  • Demo applications and instructions (net.sf.extrm.demos.*)

As for the interceptor's deployment, it follows the related CORBA ORB interface's Java mapping convention. The class RMIInterceptorControlInitInfo manages and loads all registered interceptor classes. Each interceptor is registered via a system property, for instance:

 -Dnet.sf.extrmi.RMIInterceptorControlInitInfo.RMIInterceptorClass.
   net.sf.extrmi.interceptors.TransactionContextPropagationInterceptor

The first part of the net.sf.extrmi.RMIInterceptorControlInitInfo.RMIInterceptorClass property is a predefined property prefix, and the second part is the interceptor's class name. In this example, the registered interceptor class is net.sf.extrmi.interceptors.TransactionContextPropagationInterceptor.

The level of transparency and the naming interface

Generally speaking, service-context and interceptor concepts are all about transparency and the separation of concerns. However, the level of transparency and the separation of concerns we'd like to pursue with this framework are purely on the design side. The goal is not to come up with something that can magically make an RMI-based commercial off-the-shelf solution suddenly support interceptor and service context. The target framework will offer neither a black-box solution nor 100 percent transparency. Rather, the framework tries to offer maximized design optimization for large-scale, distributed, Java RMI-based application development. The solution is meant to be lightweight and protocol-independent, and leaves nothing behind the scenes.

RMI interceptor support is not a new concept. RMI/IIOP supports an interceptor via its Portable Interceptor API. Jini Extensible Remote Invocation (JERI) offers a certain level of interceptor support too. Open source implementations are also available, such as the Aroma System. From an application design viewpoint, those solutions include the following limitations:

  • Protocol-dependent or RMI implementation-dependent, which doesn't comply with the standard RMI implementation
  • The APIs are complicated and target specific application environments
  • Underlying mechanism is not exposed to application developers (this is hardly a good thing when the application development starts to look into interceptor-based design approaches)

Existing RMI interceptor support is usually realized on the transport layer or within the RMI protocol implementation layer. The first approach has limited usage considering its lack of access to the application's context. The latter simply ties the solution to a particular RMI implementation. Another approach, however, is to realize the interceptor support on top of an RMI layer, the approach adopted by this framework.

One thing to point out is that the framework addresses only a request-level interceptor; a message-level interceptor could also be supported in a limited manner. On the other hand, a naming-reference interceptor, such as an IORInterceptor (object reference template), is not considered in this framework.

Now I describe the level of transparency provided by this framework. Usually, transparency of an RMI extension starts from naming-service customization. However, as described in my previous article, the approach taken here is to encapsulate any existing RMI naming functions and provide a single entry point for installing interceptor-enabled RMI stubs. With this design approach, a customized naming service (including IOR (Interoperable Object Reference) template) becomes unnecessary and no interference results with standard RMI functions and semantics such as dynamic classloading, distributed garbage collection, security, and activation. This approach also provides maximized interoperability and portability because no wire protocol or remote reference format is altered.

The framework implementation supports three naming encapsulation designs:

  1. Explicit factory interface to produce an interceptor-enabled stub from the original RMI stub

    (

    net.sf.extrmi.naming.RemoteInterfaceInterceptorFactoryInterface

    )

  2. A wrapper on a user-supplied generic naming interface that uses a string name as key (net.sf.extrmi.naming.RMINamingInterface and net.sf.extrmi.naming.RMINamingManagerInterface)
  3. A server-side naming (Factory/Home) interface where you can obtain the required interceptor-enabled stub (net.sf.extrmi.naming.RMINamingRemoteInterface)

Obviously, the first design offers the least transparency. The second design doesn't provide full client-side transparency. The third provides full client-side transparency, but requires a bit more work on the server-side. For all three designs, the stub returned from the naming encapsulation interface is a dynamic proxy that wraps the original RMI stub, which is RMI protocol-dependent.

The reason the third approach offers the best transparency is because applications rarely receive an RMI reference directly from the RMI naming service. The RMI naming service usually provides only the bootstrap naming support; the server-side Factory or Home interface is then involved in retrieving the required RMI stub as a return or parameter value (in call-back cases). On the server, for each naming lookup, a new dynamic proxy instance is created and returned. Such a function is totally transparent to the client.

A Java dynamic proxy is Serializable. The serialized dynamic stub proxy contains the following data:

  1. Dynamically generated proxy instance (java.lang.reflect.Proxy implements Serializable)
  2. The invocation handler instance (ClientRMIInterceptorControl implements Externalizable)
  3. The raw RMI stub (marshaling/unmarshaling is left to RMI runtime)

The invocation handler is instantiated for each Remote interface lookup and accompanies the lifecycle of the raw RMI stub. Similar to the interceptor, the invocation handler is supposed to be stateless and reentrant. Required states are always associated with the application execution context and are propagated between client and server as service contexts. The related server-side design and naming function implementation details can be found in the source code's Javadoc and my previous article.

Compared to CORBA Portable Interceptors, in the current design, service-context is propagated explicitly for each type of service-context by the corresponding interceptor. Such a design is more flexible and efficient. Interface-level interceptor configuration is also being considered even though it's not required by CORBA Portable Interceptors.

During the framework's design and implementation, I leveraged a few J2SE RMI implementation runtime behaviors:

  • Naming services create a new stub instance for each naming lookup
  • RMI remote reference unmarshaling always creates a new stub instance
  • RMI/JRMP (Java Remote Method Protocol) and RMI/IIOP stub implement all Remote interfaces declared in the server implementation class
  • J2SE 5 generates RMI stub (JRMP) class on the fly (no pregenerated stub class) and shares the same above behaviors

Conclusion and outlook

The framework discussed in this article provides a simple POJO (plain old Java object) solution for extending Java RMI. Currently, the framework implements only two interceptors, a simple logging and tracing interceptor, and a transaction-context propagation interceptor. Other common-purpose interceptors are being considered, such as interceptors that support RMI multicast (for replication) and RMI queuing (for load throttling and flow control). As you can see, all types of useful and interesting features can be easily incorporated into this framework.

Wenbo Zhu joined Sun Microsystems in 1997. For the past three years, he has been developing carrier-grade network management application platform for Nortel as a senior Java designer. He's also studying as a part-time Ph.D. student in Carleton University in Ottawa, Canada.

Learn more about this topic

Related: