sing proxies or the proxy design pattern isn't new to Java, but the new java.lang.reflect.Proxy class, introduced in JDK 1.3, can be used to create the illusion that a Java interface is a distributed object. If you do CORBA or Enterprise JavaBean development, the client code you develop doesn't talk directly to a distributed object. Instead, the object reference you have is a local proxy for the distributed object. Its interface is the same as the distributed object and, therefore, could be treated as if it were the distributed object itself.
This is a fundamental characteristic of a proxy: It looks and acts like the real thing. While in Java, these proxies existed in the distributed realm and were usually generated automatically (for example, by way of an IDL compiler), but java.lang.reflect.Proxy lets you dynamically create proxies for other objects, too.
Characteristics of a Proxy
A proxy instance can be created for one or more Java interfaces. In the case of one interface, the proxy instance could be used to represent a single object. In the case of multiple interfaces, a proxy object could act like a single entry point to a group of objects. The JDK documentation explains the rules regarding the case when a similar method signature is defined in multiple interfaces. Here, we'll focus on proxies that implement a single interface.
A proxy instance will implement the methods in the specified interfaces. In other words, code that invokes these methods on a proxy instance will compile cleanly. A proxy instance can be cast to the interface type. The instanceof operator will answer true if a proxy instance is tested against the interface for which it is a proxy. This means that anything you can do with a real instance of an interface, you can do with a proxy instance.
The JDK 1.3's new proxy class offers many benefits, some of which have been discussed already in this magazine. (See "Decorating with Java" by Dan Malks in the October issue.) One of the benefits of a proxy is that it allows you to add new behavior to a class while keeping the code for this behavior decoupled from that class. For example, a common debugging technique is to place code at the beginning and end of methods that will print a message to a log every time the method is entered and exited. We would want this logging to occur while we're testing and debugging problems, but, for efficiency, we wouldn't want this to happen in production; therefore, some flag could exist that indicates whether to perform this logging. Just about every method will have the following format:
public void foo()
{
if (System.getProperty("TRACE_ON"))
System.out.println("START: foo()");
// Actual code goes here...
if (System.getProperty("TRACE_ON"))
System.out.println("FINISH: foo()");
}
One problem with this approach is that the developer has to write these extra lines of code accurately for every method that needs to be traced. If you have hundreds of methods, it could be time consuming, error prone, and just a plain pain in the neck to maintain. A second problem is that even if tracing is turned off, every method being traced has to check the flag twice. This may result in a minorbut unnecessaryperformance decline. Ideally, we want to minimize the coding hassle and eliminate any performance decline if tracing is turned off. Let's see how we can do this with proxies.
A Proxy for Method Tracing
In our example, we'll develop an object that represents a bank account. An interface Account (see Listing 1) could be created with straightforward methods for deposits, withdrawals, and balance retrievals. A concrete class, AccountImplementation, implements this interface. It behaves the way you would expect a normal bank account to behave, including checking to ensure you don't overdraw from the account.
Following the factory design pattern, an AccountFactory class exists to create Account instances. In the factory you'll see that, depending on the TRACE_ON property, either an AccountImplementation instance is returned or a proxy for an AccountImplementation object is returned. In both cases, the factory creates an AccountImplementation instance. If the trace flag has been set, though, a TracerInvocationHandler is created with a reference to Account. Then, a static method in Proxy is called to create a proxy instance. This proxy instance will implement the Account interface and use tracer to handle the method calls sent to it. The proxy instance is cast immediately to the Account type, the account variable is set to refer to it, and the proxy instance is eventually returned. The piece of code that called this factory method has no idea that it is getting back a proxy. Hey, that's the whole point of a proxy!
So, what's the TracerInvocationHandler? A glance at Listing 2 shows that it implements the java.lang.reflect.InvocationHandler interface, which simply has an invoke() method. As arguments, it takes in the proxy instance that received the method call, the method being invoked, and the arguments for the method. How does the invocation handler know what the real instance is? I suggest simply passing it in as a constructor parameter, as I did, and having the invocation handler hold it as an instance variable.
Let's take a closer look at what the TracerInvocationHandler does. First, it logs the name of the method and the fact that we're entering it. It invokes the method on the real instance and saves any return data. Then, it logs the fact that we're exiting the method. You can see that all exceptions are caught, noted in the log, and then rethrown.
 | |
Figure 1. Running the Tester without Tracing Click here. |
Listing 3 shows some code for testing all of this. The main routine in Tester checks for a special command-line argument (-trace) to turn tracing on or off. It then gets a new account from the factory. Depending on the TRACE_ON property, it either gets back an AccountImplementation instance or a proxy instance. It then sends messages to the account. Figure 1 shows what happens when the Tester is run without tracingno tracing occurs. All we get is the stack output of the InsufficientFundsException. Notice that no proxy exists in the stack. Next, the tester is run with the -trace parameter. In this case, all the method calls are logged. Notice too that the exception stack shows that the method calls were being routed through a proxy.
What makes this better than simply putting traces in our methods? The TracerInvocationHandler can be used for any interface. This one class has all the tracing code you need, thereby eliminating having to code and maintain extra code for potentially hundreds of methods. Second, the factory creates the proxies only if tracing is turned on; otherwise, the real instances are used. Therefore, in production, when tracing is turned off, no extra performance hit is realized.
Know Thyself
A closer look at Figure 1 reveals that not all the method calls were traced. Specifically, the AccountImplementation class doesn't go through the proxy when it sends a message to itselfthis.getBalance(). To solve this problem, any object that is a proxy target must send messages to its proxy instead of to itself; thus we'll introduce a new interface named ProxyTarget:
public interface ProxyTarget
{
public void setSelf(Object object);
}
|  |
Figure 2. Running the Tester with Enhanced Code Click here. |
The setSelf() method is all that exists in this interface. It is a way to tell a proxy target what object to use (the proxy) to refer to itself. Listing 4 provides an updated Account interface that now extends ProxyTarget and an updated AccountImplementation that implements the new method. Note that it has a new instance variable of self that is initialized to this but gets overridden in the setSelf() implementation. Now, instead of using this whenever it needs to send a message to itself, it uses selfthe self.getBalance() call. The only additional task that needs to be done is to call setSelf() in the factory. When we run the Tester again using the enhanced code, you'll see the additional getBalance() trace (see Figure 2).
This one use of Java's new Proxy class demonstrates how it fills a hole that existed in the language. Still other uses could be devised. It's clearly a powerful tool with many possibilities.
Julian Macri has done object-oriented development in C++, Smalltalk, and Java for seven years. He now develops investment banking support systems at Bank of America. He can be reached at luvmusic@alum.mit.edu.