3.4 Dynamic Proxies
Dynamic proxies, available since SDK 1.3, are the exact opposite of reflective invocation. With reflective invocation, a client uses a generic API to call methods (on a server class) that are not known at compile time. With dynamic proxies, a server uses a generic API to implement methods on a server class that is manufactured at runtime to meet a client's specification. Dynamic proxies are chameleons that take the shape of any set of interfaces desired by the client. When combined with reflective invocation, dynamic proxies can implement generic interceptors. An interceptor is a piece of code that sits between a client and a server and adds an additional service, such as transaction enlistment, auditing, security checking, or parameter marshalling. A generic interceptor requires no compile-time knowledge of the client or server APIs being intercepted.
3.4.1 Delegation instead of Implementation Inheritance
Generic interception makes it possible to use an object-oriented style based not just on inheritance, but also on delegation. Consider an entity class that accesses employee information from a database. If you wanted to use inheritance to layer in transaction enlistment, auditing, and security checks, you would need to use multiple inheritance. After pointing out that multiple implementation inheritance does not exist in Java, you might also object to the fact that this design would require eight different concrete subclasses of Employee, one each for every possible service being turned on or off, as shown in Figure 33. In general, adding another service doubles the number of concrete subclasses you need.
Figure 33 Implementation inheritance causes class proliferation.
With delegation, each new service adds only one class, and classes are simply chained together to provide the exact mix of services needed, as shown in Figure 34.
Figure 34 Delegation requires only one class per service.
3.4.2 Dynamic Proxies Make Delegation Generic
Historically, the problem with the delegation model was how to make each service generic. For example, an Audit class would need to implement EmployeeItf when it was dealing with an Employee entity, but it would need to implement InventoryItf when it was dealing with the Inventory entity. Also, the service classes would need to implement interfaces that had not even been designed yet. Dynamic proxies neatly solve this problem by allowing the transaction class to manufacture an implementation of whatever interface the client expects at runtime.
To manufacture a dynamic proxy, you need only call Proxy.newProxyInstance, passing in an implementation of the InvocationHandler interface. Summaries of these classes appear in Listing 320. The newProxyInstance method manufactures a new binary class in memory. This special class implements all the interfaces passed in the itfs array by forwarding every single method to an instance of InvocationHandler. Then, the new class is loaded into the virtual machine by the class loader specified by ldr, and it is used to construct a proxy instance that forwards to handler.
Listing 320 Key Elements of Proxy and InvocationHandler
package java.lang.reflect; public class Proxy { static Object newProxyInstance(ClassLoader ldr, Class[] itfs, InvocationHandler handler) throws IllegalArgumentException; //remainder omitted for clarity } public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
3.4.3 Implementing InvocationHandler
Dynamic proxies allow you to implement a single invoke method on the invocation handler and then use it to service any interface you choose at runtime. For example, consider an InvocationHandler that logs method calls, shown in Listing 321. This LoggingHandler class provides a trivial implementation of any interface that simply logs method calls as they are made. DemoLogging demonstrates using the LoggingHandler to log calls to DataOutput. One possible use for LoggingHandler is during development, when you need to stub out an interface that you have not yet implemented.
Notice that the toString method is treated specially. In addition to any interface methods, dynamic proxies always forward the Object methods toString, hashCode, and equals to the handler. In this case, the proxy's toString method is invoked by the handler's call to System.out.println. If the toString method were not special-cased, the call to toString would trigger the invoke method of LoggingHandler, which triggers another call to toString, and so on, recursing until the stack overflowed.
Listing 321 LoggingHandler
import java.lang.reflect.*; public class LoggingHandler implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("toString")) { return super.toString(); } System.out.println("Method " + method + " called on " + proxy); return null; } } import java.lang.reflect.*; import java.io.*; public class DemoLogging { public static void main(String [] args) throws IOException { ClassLoader cl = DemoLogging.class.getClassLoader(); DataOutput d = (DataOutput) Proxy.newProxyInstance(cl, new Class[] {DataOutput.class}, new LoggingHandler()); d.writeChar('a'); d.writeUTF("stitch in time"); } }
3.4.4 Implementing a Forwarding Handler
Although "standalone" dynamic proxies such as LoggingHandler are useful, they suffer from a major limitation in dealing with return values. Because they are totally generic, InvocationHandlers have no idea how to generate a legitimate return value for a method call. LoggingHandler finesses this issue by always returning null, which the generated proxy class will coerce to the return type of the interface method. In Listing 321, the DataOutput methods happen to return void, so the generated proxy simply ignores the return from LoggingHandler's invoke method. This coincidence will not hold up in more complex cases, those in which methods might return any type, and the compile-time type might need to be further constrained at runtime in accordance with the documented semantics of the method. In order to reasonably mimic any arbitrary interface, a dynamic proxy will either need to know the semantics of the interface, or it will need to forward the method call to some other object that does. Since the raison d'être of a proxy is to be generic, knowing the specifics on an interface is not an option. Instead, most dynamic proxies are used to forward calls to other objects.
The strength of dynamic proxies is method call forwarding. A dynamic proxy can intercept a method call, examine or modify the parameters, pass the call to some other object, examine or modify the result, and return that result to the caller. When correctly configured, dynamic proxies work transparently without the knowledge of either the client or server code.
Figure 35 shows how a dynamic proxy enables generic services. A generic service implements only one method, InvocationHandler.invoke, and forwards the call using only one method, Method.invoke. Without changing, or even reading, any existing implementation code, you can insert a dynamic proxy between two objects to inject some additional service.
Figure 35 Dynamic proxies enable generic services.
3.4.5 The InvocationHandler as Generic Service
To appreciate the power and simplicity of this model for reuse, imagine the following scenario: A large server application has been ported to Java and continues to access legacy code through a bridge that presents the legacy code as a set of Java interfaces. Unfortunately, the legacy code was written in a pointer-based language and experiences occasional memory corruption. The specific symptom is that methods sometimes return the java.util.Date "Thu Dec 25 07:42:41 EST 1969."5 Your task is to guarantee that this bug does not introduce corrupt data into the application.
With dynamic proxies, you can add an interceptor that traps all attempts to return the offending value, as is shown in Listing 322. Here, a TrappingHandler forwards all method calls to its delegate. If the delegate functions unexceptionally and returns an object, the handler checks to see if it is a Date instance indicative of the memory corruption bug. If it is, then the handler might throw an error, as the example shows, or it might take whatever other action may be necessary.
Listing 322 TrappingHandler
import java.lang.reflect.*; import java.util.*; public class TrappingHandler implements InvocationHandler { //BAD_DATE is "Thu Dec 25 07:42:41 EST 1969" //This value corresponds to an error code on some systems public static final Date BAD_DATE = new Date(-559038737); private Object delegate; public TrappingHandler(Object delegate) { this.delegate = delegate; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { result = method.invoke(delegate, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } if (result instanceof Date) { Date d = (Date) result; if (d.equals(BAD_DATE)) { throw new Error("Corrupted date " + d); } } return result; } }
3.4.6 Handling Exceptions in an InvocationHandler
It is important to code carefully against the possibility that the delegate will itself throw an exception. Because the handler is generic, you have no compile-time knowledge of what particular exceptions the delegate might throw. However, since the delegate's methods are invoked via reflection, any exception will be wrapped by an InvocationTargetException. You should not let this exception percolate out of the dynamic proxy code.
The dynamic proxy provides some help here. Since a proxy wants to be transparent to the client, it will only permit exceptions that the client expects. To enforce this, a proxy compares any thrown exception to the list of checked exceptions for the current method. If an exception is not in the list, a proxy will wrap it in an UndeclaredThrowableException, which is a RuntimeException subclass that signals a programmer error.
To complete the illusion of transparency, your InvocationHandler must not give the proxy any reason to throw an UndeclaredThrowable Exception. Therefore, the canonical implementation of a forwarding proxy includes a try/catch block that catches the InvocationTargetException and then extracts and throws the underlying exception, which is the one the client expects. Refer back to Listing 322, which demonstrates this.
3.4.7 Either Client or Server Can Install a Proxy
Either the client or the server code can wrap suspicious objects with the TrappingHandler before using them. In the TestTrappingHandler example (Listing 323), the client wraps an instance of the Test interface. If either getGoodDateValue or getBadDateValue of the Test interface return a bad date, the handler will protect the client by throwing an exception. More importantly, the same TrappingHandler can be used throughout a system to protect any number of different interfaces and implementation classes.
You could make the proxy code transparent to both client and server by using an object factory to hide the details of connecting client and server code.
Listing 323 Testing a TrappingHandler
//Test.java import java.util.*; interface Test { Date getGoodDateValue(); Date getBadDateValue(); } //TestTrappingHandler.java import java.lang.reflect.*; import java.util.*; public class TestTrappingHandler implements Test { public static void main(String [] args) { TestTrappingHandler t = new TestTrappingHandler(); System.out.println("Testing unwrapped object.\n" + "This should permit date value " + TrappingHandler.BAD_DATE); executeTests(t); Test wrap = (Test)Proxy.newProxyInstance( TestTrappingHandler.class.getClassLoader(), new Class[]{Test.class}, new TrappingHandler(t)); System.out.println("Testing wrapped object.\n" + "This should reject date value " + TrappingHandler.BAD_DATE); executeTests(wrap); } public Date getGoodDateValue() { return new Date(); } public Date getBadDateValue() { return TrappingHandler.BAD_DATE; } public static void executeTests(Test t) { System.out.println(t.getGoodDateValue()); System.out.println(t.getBadDateValue()); } }
3.4.8 Advantages of Dynamic Proxies
Dynamic proxies do not provide any service that you could not provide yourself by hand-coding a custom delegator class for every different interface. The advantage of dynamic proxies over hand-rolled delegators is twofold. First, you do not have to write dynamic proxies. Second, they can be generated on-the-fly at runtime to handle new interfaces as they appear. Because dynamic proxies are generic, they tend to be used for services that do not rely on any specific knowledge of the interface or method being forwarded:
Parameter validation or modification, where a parameter value is of interest regardless of where it appears, as in the example above
Security checks, some of which can be made based on the identity of the user or the source of the code, not the particular method being called
Propagation of "implicit" or "context" parameters, such as the transaction ID, which is automatically handled by an EJB container and does not appear in any interface declaration
Auditing, tracing, or debugging of method calls
Marshalling a Java method call to some other process, machine, or language
Like any generic tool, dynamic proxies may be inefficient compared to a solution hand-tuned to a particular problem. Situations in which performance considerations may rule out the use of dynamic proxies are discussed in §3.5.