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.
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.
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
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
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 (
- Service-context support (
- Naming interface and dynamic stub proxy factory (
- Server-side RMI interceptor support implementation (
- Common-purpose interceptor classes (
- Demo applications and instructions (
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:
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
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:
Explicit factory interface to produce an interceptor-enabled stub from the original RMI stub
- A wrapper on a user-supplied generic naming interface that uses a string name as key (
- A server-side naming (
Home) interface where you can obtain the required interceptor-enabled stub (
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
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:
- Dynamically generated proxy instance (
java.lang.reflect.Proxy implements Serializable)
- The invocation handler instance (
ClientRMIInterceptorControl implements Externalizable)
- 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
Remoteinterfaces 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.
Learn more about this topic
- "Service-Context Propagation over RMI," Wenbo Zhu (JavaWorld, January 2005)
- Ext RMIRMI Interceptor and Service-Context, a SourceForge.net open source project
- Java RMI specification
- Dynamic Proxy Classes
- CORBA Portable Interceptors
- Object Reference Template Specification
- For a solution inspired by Wenbo Zhu's design approach, read "Create an Application-Wide User Session for J2EE," Kåre Kjelstrøm (JavaWorld, March 2005)
- For more articles on RMI, browse the RMI/RMI-IIOP section of JavaWorld's Topical Index
- For more articles on CORBA, browse the CORBA section of JavaWorld's Topical Index