Interceptors for Java™ Remote Method Invocation
Weiliang Li (Lehigh University) and Dale Parson (Agere Systems)
Proceedings of the 2001 International Conference on
Parallel and Distributed Processing Techniques and Applications
Volume II, p. 850-856.
Computer Science Research, Education, and Applications Technology Press
Las Vegas, Nevada, June 25-28, 2001.
Interceptors for Java™ Remote Method Invocation
Weiliang Li
wel3@eecs.lehigh.edu
Lehigh University, EECS Dept.
19 Memorial Drive West
Bethlehem, Pa. 18015
Keywords
interceptor, CORBA, Java, remote procedure
call, remote method invocation
Abstract
A software interceptor is a procedure or object
that interposes itself between an invoking client and an
invoked server software entity. Procedural interceptors
can redirect procedure invocations to alternative
procedures that are selected dynamically at run time.
Procedural interceptors are useful for program
profiling, tracing, dynamic delegation of operation
implementation,
and
interactive
debugging.
Distributed interceptors for remote procedure calls
support additional capabilities such as protocol
bridges, server thread schedulers, fault tolerant
replication, security audits and encryption, and
performance enhancements such as compression and
caching. CORBA, the Common Object Request Broker
Architecture, comes with standard portable
interceptors, but interceptors are missing from Java
Remote Method Invocation (RMI). Java reflection
assists adding custom interceptors to RMI by enabling
a generator to create interceptor classes automatically.
These classes interpose on both the client and server
sides of RMI invocations, supporting interceptor-based
extensions that are transparent to client code.
1. Introduction to interceptors
A software interceptor is a procedure or
object that interposes itself between an invoking
client and an invoked server software entity, e.g.,
between a calling and a called procedure. A client
invokes an interceptor using the same operation
Dale Parson
dparson@agere.com
Agere Systems
1247 South Cedar Crest Blvd.
Allentown, Pa. 18103
names and parameter types and values that it
would use to invoke a server directly. Indeed a
client is usually unaware that it is invoking an
interceptor.
Once invoked, an interceptor performs some
analysis or transformation on input parameters.
An interceptor may then invoke the server
operation of the same name, using the original
parameters or transformed parameters. When the
server operation returns, the interceptor may
analyze or transform its results before returning
them to the invoking client. Alternatively, an
interceptor may bypass the intended server
operation altogether, performing its own
computations and returning results to the client.
A key feature of interceptors is dynamic
interposition. An interceptor introduces a new
layer into a layered software system by
interposing itself between clients and servers
during program execution. Transparent, dynamic
interposition requires support from the run-time
system that executes a target program. The runtime system provides some means for an
interceptor to intercept invocations that are en
route from clients to servers.
In Listing 1 we give a simple example of
procedural interception using the interpreted Tcl
scripting language [1]. We start out by exercising
a procedure avglist that computes the mean of a
list of input numbers. After determining that this
procedure works as expected, we interpose an
interceptor by moving avglist into namespace
hidden, and then defining a new procedure avglist
— the interceptor — that records the maximum
input number passed from any client, after which
the interceptor invokes the original procedure and
returns its result. Once the interceptor is installed,
we can check global variable max to determine
the maximum number passed. An alternative
interceptor might bypass the original avglist,
returning the median or mode instead of the mean.
Client code that invokes avglist remains unaware
that interception is occurring.
proc avglist {numbers} {# average of
set sum 0.0
; # numbers
set count 0
foreach entry $numbers {
set sum [expr $sum + $entry]
incr count }
return [expr $sum / $count] }
% avglist {2 4 6 8}
5.0
% # Now interpose an interceptor.
rename avglist hidden::avglist
proc avglist {numbers} {
global max
foreach entry $numbers {
if {$entry > $max} {
set max $entry }}
return [hidden::avglist $numbers] }
% set max -1000 ; # initialize
-1000
% avglist {2 4 6 8}
5.0
% echo $max ; # check intercepted max
8
Listing 1: A simple Tcl interceptor
Procedural interception in an interpreted
language like Tcl is possible because the program
interpreter resolves procedure name-to-code
bindings at run time. An interceptor can alter a
binding by renaming a procedure, by moving it to
a private namespace, or by using some other
language-specific mechanism. The interceptor
creates its own name-to-code binding in place of
the original. There can be multiple levels of
interceptors for a single server procedure, where
each interceptor remains unaware of the existence
of other interceptors. The rename command in
Listing 1 may in fact be renaming an interceptor.
As long as each interceptor uses a different
namespace to hide its “original” command,
interceptors can nest to arbitrary depth.
Run-time environments for compiled
programs use other mechanisms to support
procedural interception. Some compiled programs
are linked so they can attach to dynamic link
libraries at program initialization time. A runtime loader binds unresolved procedure
references in such programs to compiled
procedures by traversing a library path in search
of libraries with appropriate names and symbols.
In such environments an interceptor can interpose
itself into invocations of library procedures by
interposing an interceptor library into the search
path [2]. A client invocation of a dynamically
loaded procedure enters the interceptor, and the
interceptor can call the original library explicitly.
Another interception mechanism for compiled
programs uses process control and trace system
calls such as /proc and ptrace() for UNIX [3]. An
interceptor acts as a special-purpose debugger that
places breakpoints in application procedures and
system calls. When a target program triggers an
interceptor’s breakpoint, the interceptor performs
its work. The interceptor can invoke or bypass the
intercepted procedure.
Procedural interceptors enable a number of
useful dynamic processing capabilities [2,4].
(i) They enable profiling. An interceptor can
count invocations, measure time spent in
invocations, measure quantity of data passed in
parameters, and measure application-specific
properties of parameters and return values.
(ii) They enable tracing. Interceptors can log
nested invocations of server procedures, thereby
enabling trace-based debugging of client-server
interactions.
(iii) They enable dynamic delegation of
operation implementation. A chain of one or more
interceptors embodies the Chain of Responsibility
design pattern [5]. This pattern is appropriate
when there may be more than one handler for an
operation, when decoupling between an operation
client and server is desired, or when late binding
of a servant to an operation is desired. Any
operation that allows a user or framework to
specify an exact operation at run time is a
potential candidate for this use of interceptors.
(iv) They enable interactive debugging. An
interceptor can invoke an interactive debugger
before or after invoking a server’s operation. The
debugger can inspect and possibly modify
parameters, returns values, and other elements of
program state.
2. RPC and CORBA interceptors
While procedural interceptors have several
uses as discussed in Section 1, they become even
more useful when applied in Remote Procedure
Call (RPC) systems such as the Common Object
Request Broker Architecture (a.k.a. CORBA)
[6,7]. Figure 1 gives an outline of distributed
procedure invocation processing in an RPC
system. A client invokes an operation on a stub
module contained within the same networked
node and process as the client. The stub marshals
parameters into a data stream according to the
protocol requirements of its transport layer, then
ships this data stream to the server process via this
transport. A server-side skeleton demarshals the
data stream back into parameters, then it invokes
the corresponding server operation. When the
operation completes, these steps are reversed. The
server skeleton marshals return values and ships
them back to the client stub via the transport, and
the stub demarshals them and returns them to the
client. CORBA expands on this basic RPC
mechanism by supporting communication among
clients and servers built using differing
programming languages and running on differing
computer architectures. CORBA also offers an
assortment of distributed processing services.
RPC increases the number of potential
interception points for an operation from two to
four. Whereas a conventional, single-process
procedure can be intercepted both before and after
invocation, a remote procedure can be intercepted
in the client process before invocation, in the
server process before and after invocation, and
again in the client process after invocation.
The presence of the network transport layer
adds several new uses for interception that are not
present in single-process applications [4].
(i) RPC interceptors can serve as bridges to
alternate
communications
protocols.
An
interceptor can remarshal data streams to conform
to protocols not envisioned by the original
distributed application designers.
(ii) RPC interceptors can serve as schedulers
by overriding default thread allocation policies in
Client Process
Client application module
operation
invocation
Distributed operation stub
transport
layer
Distributed operation skeleton
operation
invocation
Server application module
Server Process
Figure 1: Remote procedure invocation
multi-threaded servers.
(iii) RPC interceptors can support faulttolerant processing by replication of redundant
servers and auditing of replicated invocations.
(iv) RPC interceptors can support distributed
processing security, for example authentication of
clients, as well as pre-transport encryption and
post-transport decryption of data streams.
(v) RPC interceptors can support performance
enhancement for distributed processing, such as
pre-transport compression and post-transport
decompression of data streams, as well as caching
of stateless distributed query results in a local
cache server.
Early versions of CORBA supported
application-specific or vendor-specific types of
interceptors [4,8]. Current CORBA includes a
vendor-neutral standard for portable interceptors
[9]. This standard allows interceptor-based
mechanisms to work with any CORBA
implementation and application.
3. Java Remote Method Invocation
Given the benefits of RPC interceptors, it is
surprising that Java’s equivalent to RPC, Java
Remote Method Invocation (RMI) [10,11], does
not provide built-in support for interceptors.
Fortunately, creating custom interceptors for Java
RMI is a relatively straightforward process. This
section outlines standard Java RMI mechanisms,
and the next section shows how we have added
custom interceptors to Java RMI.
Figure 2 is a UML class diagram for a set of
Java interfaces and classes that represent those
required in any RMI-based distributed system.
Java interfaces, marked with the «interface» tag,
specify operation signatures, but they contain no
executable code. The remaining boxes signify
Java classes. A line with a closed arrow represents
inheritance, while a line with an open arrow
indicates reference from one class or interface to
another. A line without an arrow represents
bidirectional reference between two classes.
Figure 2 shows RMI’s built-in interface
java.rmi.Remote serving as a base interface for
any distributed server, represented by applicationspecific interface App in our example. App is a
place holder for any application-specific interface
that specifies a server’s distributed operations.
AppImpl is a server class that implements the
operations specified by App. Inheritance
establishes the fact that AppImpl satisfies App’s
contract, i.e., any AppImpl object is an instance of
an App object. AppImpl also inherits from (i.e.,
extends) library class UnicastRemoteObject. The
latter class provides networking infrastructure
needed to make AppImpl’s operations available
for distribution.
Once AppImpl has been compiled into a Java
class file, RMI’s rmic compiler utility reads the
class file and generates classes AppImpl_Stub, a
client-side stub class, and AppImpl_Skel, a
server-side skeleton class, as seen in Figure 2. The
compiled AppImpl class file gives rmic the
information it needs to identify the distributed
operations, namely, all operations belonging to
parent interfaces from which AppImpl derives,
that
inherit
in
turn
from
interface
java.rmi.Remote. In Figure 2 interface App
specifies these operations. Rmic generates stub
methods for each of these operations into class
AppImpl_Stub, and it generates corresponding
skeleton code into AppImpl_Skel.
AppClient of Figure 2 is an example of a
client class that uses the AppImpl_Stub to make
remote method calls. The application logic of
AppClient can be coded so that it does not depend
on invoking a distributed, AppImpl_Stub method.
Instead, AppClient uses some object that
implements the App interface. This object may be
an actual AppImpl object for a non-distributed
system, or it may be an AppImpl_Stub object that
also implements the App interface by delegating
method calls to a remote AppImpl object.
java.rmi.server.UnicastRemoteObject
java.rmi.Naming
<<static>> lookup() : Remote
<<static>> rebind(name : String, obj : Remote)
<<Interface>>
java.rmi.Remote
AppImpl
AppClient
operation()
<<Interface>>
App
operation()
AppImpl_Stub
AppImpl_Skel
operation()
Figure 2: Representative interfaces and classes for a Java RMI-based system
client :
AppClient
stub :
AppImpl_Stub
nameServer :
java.rmi.Naming
skeleton :
AppImpl_Skel
server :
AppImpl
rebind(String, server)
server rebinds itself
lookup(name:String)
returns an AppImpl_Stub
reference
operation( )
operation( )
potential interception points
Figure 3: Sequence of steps in connecting an RMI client to a server
The final class of Figure 2, java.rmi.Naming,
helps in the initial connection of a client to a
server as outlined in the UML sequence diagram
of Figure 3. First the server object uses the
Naming.rebind operation to bind itself to a
symbolic name in the Naming service; rebind
associates an AppImpl_Skel object with the name.
When AppClient invokes Naming.lookup to
retrieve an object associated with that name that
implements the App interface, AppClient gets
back an AppImpl_Stub object. Thereafter,
interactions between AppClient and AppImpl
occur largely as they would for any other RPC
system. AppClient invokes distributed operations
on its AppImpl_Stub object, which the latter
forwards via transport to the AppImpl_Skel object
for delegation to the AppImpl server.
4. Interceptors for Java RMI
Figure 4 shows a modified version of the class
diagram of Figure 2 that illustrates our custom
RMI interceptors. Rather than distribute the
AppImpl class, we generate and distribute an
AppInterceptor class that constitutes the serverside interceptor. Our interceptor generator uses
Java reflection [12] to determine the methods of
AppImpl that belong to remote interfaces derived
from java.rmi.Remote. Java reflection gives a
program such as our generator the ability to
inspect a class’s ancestor classes and interfaces,
along with methods, parameter types and return
types. Our generator uses reflection to navigate up
from any compiled class derived from
java.rmi.Remote to determine its RMI-distributed
operations. AppInterceptor’s constructor takes an
AppImpl object reference as a parameter, and the
generator creates an AppInterceptor method for
each of AppImpl’s distributed methods. An inert
version of AppInterceptor simply passes method
invocations on to corresponding methods of the
AppImpl object, but a custom class derived from
AppInterceptor can supply redefinitions of some
or all of these server-side interceptor methods.
We now apply rmic to AppInterceptor,
distributing the server interceptor in addition to
the actual server. In fact we can distribute a class
derived from AppInterceptor that adds
interception-specific methods, such as interprocess synchronization or thread identification
operations, to the set of distributed methods.
AppImpl and AppClient methods remain unaware
of these added interception “back door” methods.
Our generator also generates the client-side
interceptor class AppClientInterceptor of Figure
4. AppClientInterceptor, like AppInterceptor, has
one method per distributed App operation. An
inert version of AppClientInterceptor passes
method invocations to corresponding methods of
java.rmi.server.UnicastRemoteObject
java.rmi.Naming
<<static>> lookup() : Remote
<<static>> rebind(name : String, obj : Remote)
<<Interface>>
java.rmi.Remote
AppClient
AppInterceptor
AppImpl
new()
operation()
operation()
AppClientInterceptor
<<Interface>>
App
new(connection : App)
operation()
operation()
AppInterceptor_Stub
AppInterceptor_Skel
operation()
Figure 4: Interfaces and classes for RMI interception
AppInterceptor_Stub. Again we can derive a
custom class from AppClientInterceptor, for
example to add invocations of the interception
“back door” methods of AppInterceptor.
Figure 5 gives the sequence diagram of our
approach. The server now constructs an
AppInterceptor object that takes its AppImpl
object as a constructor parameter; the server
passes the AppInterceptor reference to
Naming.rebind for distribution. The client now
retrieves an AppInterceptor_Stub object reference
from Naming.lookup, and the client passes this
stub reference to an AppClientInterceptor
constructor. Thereafter, the sequence of
distributed operation invocations occurs as
diagrammed in Figure 5. Invocation flows from
AppClient, through AppClientInterceptor, the
AppInterceptor_Stub and the transport to
AppInterceptor_Skel, on to AppInterceptor and
finally to AppImpl. Return results follow the
reverse route.
The prototype implementation of our
approach requires small source code changes to
the original App server and client code. Our
prototype explicitly constructs an AppInterceptor
object with an AppImpl parameter, and it
explicitly constructs an AppClientInterceptor
within AppClient code. The latter modification to
client code is especially odious, since client-side
transparency of interception is very desirable as
part of a modular design. A better solution is for a
client to invoke an Abstract Factory Method [5] to
obtain its App interface object. An abstract
factory hides construction details from the client,
returning some object that implements interface
App. By decoupling interceptor and stub
construction from AppClient source code, the
system regains it modularity and it gains
flexibility in selecting from alternative custom
interceptors at run time. The result is a very
powerful mechanisms for exploring the
application space of Java RMI interception.
5. Conclusions and directions
Interceptors have a proven record for dynamic
program monitoring and extension. Given their
usefulness and their appearance in distributed
processing standards such as CORBA, they
promise to be equally useful for RMI-based
systems. Fortunately, RMI is based on generation
of modular distributed communication classes
from interface specifications, an approach that is
straightforward to extend. Java reflection allows
us to automatically generate interceptor shell
classes that we can extend through inheritance
and dynamic constructor selection.
Our application areas of interest are
debugging and profiling. While the Java Platform
client :
AppClient
CliInter :
AppClientInterceptor
stub :
AppInterceptor_Stub
nameServer :
java.rmi.Naming
skeleton :
AppInterceptor_Skel
srvinterceptor :
AppInterceptor
server :
AppImpl
new(server)
rebind(String, srvinterceptor)
server rebinds server
interceptor "srvinterceptor"
to distributed name
lookup(name:String)
returns an AppInterceptor_Stub reference
new(stub : App)
operation( )
operation( )
operation( )
operation( )
Figure 5: Sequence of steps for RMI interception
Debugger Architecture [13] provides excellent
support for debugging individual Java Virtual
Machine (JVM) processes, it provides no support
for debugging systems distributed over RMI. We
are using the RMI interceptor mechanisms
described in this paper to explore coordinated
debugging of multiple distributed JVMs, with
interceptors communicating parameters, results,
state information and processing events to a
central debugger that is also written in Java.
6. References
1. B. Welch, Practical Programming in Tcl and
Tk, Third Edition. Upper Saddle River, NJ:
Prentice Hall PTR, 1999.
2. T. Curry, “Profiling and Tracing Dynamic
Library Usage via Interposing,” Proceedings of
the Summer 1994 USENIX Conference.
Berkeley, CA: USENIX, June, 1994.
3. J. Rosenberg, How Debuggers Work. New
York, John Wiley & Sons, 1996.
4. P. Narasimhan, L. Moser and P. M. MelliarSmith, “Using Interceptors to Enhance
CORBA,” IEEE Computer, July, 1999, p. 62-68.
5. E. Gamma, R. Helm, R. Johnson and J.
Vlissides, Design Patterns, Elements of
Reusable Object-Oriented Software. Reading,
MA: Addison-Wesley, 1995.
6. The Common Object Request Broker:
Architecture and Specification, Rev. 2.2, Object
Management Group, Framingham, Mass., 1998;
ftp://ftp.omg.org/pub/docs/formal/98-07-01.pdf.
7. J. Siegel, CORBA Fundamentals and
Programming. New York, John Wiley & Sons,
1996.
8. VisiBroker 4 Features and Benefits, http://
www.inprise.com/visibroker, 2000.
9. Object Management Group, Portable
Interceptors Joint Submission, OMG documents
orbos/99-12-02 and orbos/99-12-03, http://
www.omg.org.
10. Sun Microsystems, Java Remote Method
Invocation,
http://java.sun.com/products/jdk/
1.2/docs/guide/rmi/spec/rmiTOC.doc.html.
11. A. Wollrath, J. Waldo and R. Riggs, “JavaCentric Distributed Computing.” IEEE Micro,
May/June 1997, p. 44-53.
12. K. Arnold and J. Gosling, The Java™
Programming Language, Second Edition.
Reading, MA: Addison-Wesley, 1997.
13. Sun Microsystems, Java Platform Debugger
Architecture, http://java.sun.com/products/jpda.