USING DYNAMIC PROXIES TO
LAYER NEW FUNCTIONALITY
OVER EXISTING CODE
Dynamic proxies allow you to implement new interfaces at runtime
by forwarding all calls to an InvocationHandler. This tip shows
you how to use dynamic proxies to add new capabilities without
modifying existing code.
Consider the following program. The program includes an interface
named Explorer. The interface models the movement of an "explorer"
around a Cartesian grid. The explorer can travel in any compass
direction, and can report its current location. The class
ExplorerImpl is a simple implementation of the Explorer interface.
It uses two integer values to track the explorer's progress around
the grid. The TestExplorer class sends the explorer on 100 random
steps, and then logs the explorer's position.
import java.lang.reflect.Method;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
interface Explorer {
public int getX();
public int getY();
public void goNorth();
public void goSouth();
public void goEast();
public void goWest();
}
class ExplorerImpl implements Explorer {
private int x;
private int y;
public int getX() {return x;}
public int getY() {return y;}
public void goNorth() {y++;}
public void goSouth() {y--;}
public void goEast() {x++;}
public void goWest() {x--;}
}
public class TestExplorer {
public static void test(Explorer e) {
for (int n=0; n<100; n++) {
switch ((int)(Math.random() * 4)) {
case 0: e.goNorth(); break;
case 1: e.goSouth(); break;
case 2: e.goEast(); break;
case 3: e.goWest(); break;
}
}
System.out.println("Explorer ended at "
+ e.getX() + "," + e.getY());
}
public static void main(String[] args) {
Explorer e = new ExplorerImpl();
test(e);
}
}
Try running the TestExplorer class. You should get one line of
output, similar to this:
Explorer ended at -2,8
Now, imagine that the requirements for the system change, and you
need to log the explorer's movement at each step. Because the
client programmed against an interface, this is straightforward;
you could simply create a LoggedExplorer wrapper class that logs
each method call before delegating to the original Explorer
implementation. This is a nice solution because it does not require
any changes to ExplorerImpl. Here's the new LoggingExplorer wrapper
class:
class LoggingExplorer implements Explorer {
Explorer realExplorer;
public LoggingExplorer(Explorer realExplorer) {
this.realExplorer = realExplorer;
}
public int getX() {
return realExplorer.getX();
}
public int getY() {
return realExplorer.getY();
}
public void goNorth() {
System.out.println("goNorth");
realExplorer.goNorth();
}
public void goSouth() {
System.out.println("goSouth");
realExplorer.goSouth();
}
public void goEast() {
System.out.println("goEast");
realExplorer.goEast();
}
public void goWest() {
System.out.println("goWest");
realExplorer.goWest();
}
}
The LoggingExplorer class delegates to an underlying realExplorer
interface, which allows you to add logging to any existing Explorer
implementation. The only change clients of the Explorer interface
need to make is to construct the LoggingExplorer so that it wraps
the Explorer interface. To do this, modify TestExplorer's main
method as follows:
public static void main(String[] args) {
Explorer real = new ExplorerImpl();
Explorer wrapper = new LoggingExplorer(real);
test(wrapper);
}
Now your output should be similar to
goWest
goNorth
...several of these...
goWest
goNorth
Explorer ended at 2,2
By delegating to an underlying interface, you added a new layer of
function without changing the ExplorerImpl code. And you did it
with only a trivial change to the test client.
The LoggingExplorer wrapper class is a good start to using
delegation, but this "by-hand" approach has two major drawbacks.
First, it's tedious. Each individual method of the Explorer
interface must be reimplemented in the LoggingExplorer
implementation. The second drawback is that the problem (that is,
logging) is generic, but the solution is not. If you want to log
some other interface, you need to write a separate wrapper class.
The Dynamic Proxy Class API can solve both of these problems.
A dynamic proxy is a special class created at runtime by the
Java(tm) virtual machine*. You can request a proxy class that
implements any interface, or even a group of interfaces, by
calling:
Proxy.newProxyInstance(ClassLoader classLoaderToUse,
Class[] interfacesToImplement,
InvocationHandler objToDelegateTo)
The JVM manufactures a new class that implements the interfaces you
request, forwarding all calls to InvocationHandler's single method:
public Object invoke(Object proxy, Method meth, Object[] args)
throws Throwable;
All you have to do is implement the invoke method in a class that
implements the InvocationHandler interface. The Proxy class then
forwards all calls to you.
Let's make this work for the Explorer interface. Replace the
LoggingExplorer wrapper class with the following Logger class.
class Logger implements InvocationHandler {
private Object delegate;
public Logger(Object o) {
delegate = o;
}
public Object invoke(Object proxy, Method meth, Object[] args)
throws Throwable {
System.out.println(meth.getName());
return meth.invoke(delegate, args);
}
}
This implementation of the invoke method can log any method call on
any interface. It uses reflective invocation on the Method object
to delegate to the real object.
Now modify the TestExplorer main method as follows to create
a dynamic proxy class:
public static void main(String[] args) {
Explorer real = new ExplorerImpl();
Explorer wrapper = (Explorer) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[] {Explorer.class},
new Logger(real));
test(wrapper);
}
The static method Proxy.newProxyInstance creates a new proxy
that implements the array of interfaces passed as its second
parameter. In this example, the proxy implements the Explorer
interface. All invocations of Explorer methods are then handed off
to the InvocationHandler that is passed as the third parameter.
Try running the updated code. You should see that each step of the
Explorer is logged to System.out.
The dynamic proxy class solves both of the problems of the
"by-hand" approach. There is no tedious copying and pasting of
methods because invoke can handle all methods. Also, the logger
presented here can be used to log calls to any interface in the
Java(tm) language. Try inserting some loggers in your own code to
trace program flow.
Notice that the logging operation is method generic, that is,
logging does not require any decision making based on the
specifics of the method being called. Dynamic proxies are at their
best when adding method-generic services. Logging is one area
where dynamic proxies can be used to advantage; others include
generic stubs for RMI, automatic parameter validation, transaction
enlistment, authentication and access control, and rule-based
parameter, modification, and error handling.
Dynamic proxies, like all reflective code, are somewhat slower than
"normal" code. In many situations this performance difference is
not crucial. If you want to evaluate the performance of dynamic
proxies for delegation, download the benchmarking code from
http://staff.develop.com/halloway/JavaTools.html and execute the
TimeMethodInvoke.cmd script. This script measures times for various
styles of method invocation in the Java language.
For more info on dynamic proxies, see
http://java.sun.com/j2se/1.3/docs/guide/reflection/proxy.html.
|