|
Articles Index
Class Methods
By Tom Harpin
July 2001
Library interposition has proven to be a very useful technique in C language
programming. With this approach, it is not necessary to recompile any existing source code--or
even have access to the source code--in order to trace, profile or debug function
calls. The goal of this technical article is to explore a technique for interposing
on Java methods.
Solaris Library Interposition
The following example uses interposition to record the frequency of malloc,
calloc and realloc system calls for various amounts of memory,
and displays the results in a histogram. A shared library called malloc_hist.so
redefines the functions: malloc, calloc, realloc as follows:
void * malloc(size_t size)
{
static void * (*func)();
void * ret;
if(!func) {
func = (void *(*)())
dlsym(RTLD_NEXT, "malloc");
}
ret = func(size);
bump_counter(&mdata, size);
return(ret);
}
void * calloc(size_t nelem, size_t elsize)
{
static void * (*func)();
void * ret;
int i;
if(!func)
func = (void *(*)())
dlsym(RTLD_NEXT, "calloc");
ret = func(nelem, elsize);
for(i=0;i<nelem;i++)
bump_counter(&cdata, elsize);
return(ret);
}
void * realloc(void *ptr, size_t size)
{
static void * (*func)();
void * ret;
if(!func)
func = (void *(*)())
dlsym(RTLD_NEXT, "realloc");
ret = func(ptr, size);
bump_counter(&rdata, size);
return(ret);
}
|
The redefined exit function collects the memory allocation information stored in some
data structures and prints it to a file.
Setting the LD_PRELOAD environment variable to (some path)/malloc_hist.so ensures that the
user-defined functions are loaded first. After some pre-processing, the system malloc functions are
loaded (if necessary) and called. For more information about LD_PRELOAD and runtime linking, see the
Linkers
and Libraries Guide.
Java Method Interposition
An equivalent technique could be very useful for developers in tracing, profiling and debugging Java programs. Proxy class methods could be interposed in order to examine
arguments, record the frequency of calls, timing information, and so forth, before or after calling the original method. This technique is not quite as simple for methods,
however, since Java is a pure object-oriented language which does not allow any stand-alone (global) functions. Methods are not loaded individually, as C functions
are,
but rather are loaded as part of the class within which they are defined. Therefore, any override must start at the class level rather than at the method level. The
CLASSPATH environment variable plays a role here similar to what LD_LIBRARY_PATH does for
C programs. It provides an ordered search path for the classloader to find a
needed classfile.
At class load time, the interposed class must be found before the original one. This can be readily accomplished through manipulation of the CLASSPATH environment
variable or a -classpath switch on the commandline. The interposed class must have the same full name as the original. For example, a class Foo may exist in package
pub.foo and create an instance of class Bar from package pub.bar. Creating a file Bar.java
in another directory, such as fake/pub/bar and prepending /fake to the classpath, ensures that the JVM will load our definition of class pub.bar.Bar rather than the
original. In this way, we can interpose our definitions of Bar's methods over the originals.
There is a problem, however, if we now wish to dispatch to the original method. Since each classloader maintains a cache of unique class names, the interposed and original
classes pub.bar.Bar cannot coexist in the same classloader's namespace. Therfore, the original class must be loaded by a separate classloader (using a different classpath)
and a technique must be devised to "trampoline" from the interposed method call to the original.
Proxy Class Interposition
Before tackling those problems, let's look at a built-in Java language facility that can be used to simulate much of the functionality provided by C language interposition.
J2SE v1.3 has introduced a dynamic proxy class in the Java Reflection package. A dynamic proxy class implements a list of interfaces specified at runtime such that a
method invocation through one of the interfaces on an instance of the class will be encoded and dispatched to another object through a uniform interface. Proxy classes and
instances are created using static methods of the java.lang.reflect.Proxy class. The Proxy.getProxyClass method returns the java.lang.Class object for a proxy class given a
class loader and an array of interfaces. The proxy class will be defined in the specified class loader and will implement all of the supplied interfaces. Each proxy class has
one public constructor that takes one argument, an implementation of the java.lang.reflect.InvocationHandler interface. Rather than having to use the reflection API to
access the public constructor, a proxy instance can be also be created by calling the Proxy.newProxyInstance method, which combines the actions of calling
Proxy.getProxyClass and invoking the constructor with an invocation handler.
For example:
public class TraceProxy
implements java.lang.reflect.InvocationHandler {
public static Object newInstance(Object obj) {
return java.lang.reflect.Proxy.newProxyInstance(
obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
new TraceProxy(obj));
}
...
}
|
Once a new proxy instance has been created, it is legal to cast it to any one of the
interfaces that it implements, and to call any method supported by that interface.
For example:
Bar b = (Bar)
TraceProxy.newInstance(new BarImpl());
b.hello();
Method invocations on an instance of a dynamic proxy class are dispatched to the
invoke method in the instance's invocation handler, and they are encoded with a
java.lang.reflect.Method object identifying the method that was invoked and an array
of type Object containing the arguments. Arguments of primitive types are wrapped in
an instance of the appropriate primitive wrapper class, such as java.lang.Integer or
java.lang.Boolean. The implementation of the invoke method is free to modify the
contents of this array.
With this facility, we may create our own proxy class which can intercept a method call,
do some pre-processing, invoke the original method, then do some post-processing.
This is exactly the method interposing behavior that we seek!
For example:
public Object invoke(Object proxy,
Method m, Object[] args) throws Throwable
{
Object result;
try {
System.out.print("begin method " +
m.getName() + "(");
for(int i=0; i<args.length; i++) {
if(i>0) System.out.print(",");
System.out.print(" " +
args[i].toString());
}
System.out.println(" )");
result = m.invoke(obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (Exception e) {
throw new RuntimeException
("unexpected invocation exception: " +
e.getMessage());
} finally {
System.out.println("end method " +
m.getName());
}
return result;
}
|
Following is the complete code for a simple example of method interposing using a dynamic
proxy class.
Foo.java
package pub.foo;
import pub.bar.*;
import pub.proxy.*;
public class Foo {
public static void main(String[] args) {
Bar bar = (Bar)
TraceProxy.newInstance(new BarImpl());
bar.hello(2001, "xxx");
bar.goodbye("yyy", 2002);
}
}
|
Bar.java
package pub.bar;
import java.io.*;
public interface Bar {
public void hello(int i, String s);
public void goodbye(String s, int i);
}
|
BarImpl.java
package pub.bar;
import java.io.*;
public class BarImpl implements Bar {
public void hello(int i, String s) {
System.out.println
(" in pub.bar.Bar.hello");
}
public void goodbye(String str, int i) {
System.out.println
(" in pub.bar.Bar.goodbye");
}
}
|
TraceProxy.java
package pub.proxy;
import java.lang.reflect.*;
import pub.bar.*;
public class TraceProxy
implements java.lang.reflect.InvocationHandler {
private Object obj;
public static Object newInstance(Object obj) {
return java.lang.reflect.Proxy.newProxyInstance(
obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
new TraceProxy(obj));
}
private TraceProxy(Object obj) {
this.obj = obj;
}
public Object invoke(Object proxy,
Method m, Object[] args) throws Throwable
{
Object result;
try {
System.out.print("begin method "
+ m.getName() + "(");
for(int i=0; i<args.length; i++) {
if(i>0) System.out.print(",");
System.out.print(" " +
args[i].toString());
}
System.out.println(" )");
result = m.invoke(obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (Exception e) {
throw new RuntimeException
("unexpected invocation exception: " +
e.getMessage());
} finally {
System.out.println(
"end method " + m.getName());
}
return result;
}
}
|
Conclusion
The proxy class is relatively flexible and straightforward to use, but there are some
distinct limitations on its use for method interposing. One limitation is that the method
must be called through an instance of the proxy class. So nested methods calls, for
instance, would not be intercepted. Another limitation is that the method must have
been defined in an Interface that is implemented by the object being proxied. It can
not be called through an instance of a class that does not implement an interface. The
next article in this series will illustrate some techniques for overcoming these limitations.
About the Author
Tom Harpin works in Market Development Engineering at Sun's
Burlington, MA campus. He works primarily on Distributed System
Management vendor integration efforts with WBEM, and JMX. Prior
to Sun, Tom helped design and implement several client-server
systems for Delta Airlines.
For More Information
Linkers and Libraries
Guide
Proxy Class
Documentation
(Scroll down to java.lang.reflect in the top left box, click, then
click on Proxy in the bottom left box)
Have a question about programming? Use
Java Online Support.
|
|