Previous Next |
Two Kinds of ProxiesWhen working with proxies, there is a wide array of strategies available to you for implementing them. One strategy is the use of pre-compile-time tools such as those used by CORBA. Also, proxies can be manually written, reflexively driven, or dynamically generated. Static ProxiesProxies that are written manually are referred to as static proxies. Static proxies are programmed into the system at compile time. Example 10-2 shows a simple static proxy. Programming static proxies is a just like coding any other class with a couple of new rules:
When writing static proxies, the programmer introduces new code into the proxy to implement the new functionality. For example, you can alter the proxy from Example 10-2 to incorporate the ability to count how many times the methods of the implementation are called. The resulting class is shown in Example 10-4. Example 10-4. A counting proxypackage oracle.hcj.proxies; public class SomeClassCountingProxy { private final SomeClassImpl impl; private int invocationCount = 0; public SomeClassCountingProxy(final SomeClassImpl impl) { this.impl = impl; } public int getInvocationCount( ) { return invocationCount; } public void someMethod( ) { this.invocationCount++; this.impl.someMethod( ); } public void someOtherMethod(String text) { this.invocationCount++; this.impl.someOtherMethod(text); } } This static proxy will keep track of the number of times the implementation's methods are invoked. You changed the original proxy and inserted content into the proxy to implement the required functionality. Using the proxy is virtually identical to using the SomeClassProxy object. Here's a modified factory-creation method: package oracle.hcj.proxies; public class SomeClassFactory { public static final SomeClassCountingProxy getCountingProxy( ) { SomeClassImpl impl = new SomeClassImpl(System.getProperty("user.name")); return new SomeClassCountingProxy(impl); } } Just as before, get the proxy from the factory and then use it as if it were the class itself: package oracle.hcj.proxies; package oracle.hcj.proxies; public class DemoCountingProxy { public static final void main(final String[] args) { SomeClassCountingProxy proxy = SomeClassFactory.getCountingProxy( ); proxy.someMethod( ); proxy.someOtherMethod("Our Proxy works!"); System.out.println("Method Invocation Count = " + proxy.getInvocationCount( )); } } Here, you access the methods in an identical fashion as the last proxy you wrote. However, this method counts the number of invocations to the methods of the implementation. In the emphasized code, you write out the invocation count. The output is shown here: >ant -Dexample=oracle.hcj.proxies.DemoCountingProxy run_example run_example: [java] Robert [java] Our Proxy works! [java] Method Invocation Count = 2 This two-tier functionality gives you a great deal of power in designing strong apps. You can insert code in between the implementation of an object and the object itself to alter the functionality of the original implementation. Also, you can add more methods to the proxy class that did not exist in the original implementation class. For example, the getInvocationCount( ) method, which does not exist in the original implementation, was added. This technique is known as a decorator pattern. Decorators are especially useful when you want to modify objects whose sources you do not control, such as third-party libraries. However, they can also be useful in creating extended functionality of an object within a particular context, as is done with method counting. Proxy by InterfaceIn previous examples, the proxies that you created required that the user know which kind of proxy he wants. This is often not a desirable situation. For example, a distributed program that could be spread across several machines would want to take advantage of a variety of proxies, depending on the location of the implementation. If the implementation is running on the same machine, the program shouldn't have to use the network to talk to the object; if the object is elsewhere on the LAN, then networking is required. However, the user of the implementation is not interested in these details. He simply wants to use the object without worrying about where the actual implementation is. What you need is a way for the factory to give the user the correct proxy based on specific information, such as where the implementation is located within a network. In Example 10-4, you created a proxy that counted the number of times the methods of the object was called. As an example of a proxy that behaves differently based on specific conditions, suppose that your counting proxy may be necessary during debugging but not during the deployment of the app. What you would like to do is give the user the counting proxy when debugging is activated and the noncounting proxy when debugging is turned off. However, the user won't know which proxy he is getting until runtime. Therefore, you need another way for the user to refer to the proxy. Java interfaces are ideally suited to solving such problems. To isolate the implementation details of the proxy, you can use an interface that describes the functionality of the implementation. When you apply this technique to the implementation class from Example 10-2, you get the result in Example 10-5. Example 10-5. An interface to an implementationpackage oracle.hcj.proxies; public interface SomeClass { public abstract void someMethod( ); public abstract void someOtherMethod(final String text); } Now that you have an interface to your implementation, you can enforce this interface by changing SomeClassImpl: public class SomeClassImpl implements SomeClass { // same as before } Additionally, you can make the proxy classes implement the interface: public class SomeClassProxy implements SomeClass { // same as before } public class SomeClassCountingProxy implements SomeClass { // same as before } Although the proxy classes implement the interface, this doesn't break the inheritance rule from earlier because the user will have only the proxy, which doesn't inherit from the implementation. If a user gets a proxy and casts it up to the interface, the virtual machine won't have a problem. However, if he tries to cast the interface to the implementation, the built-in RTTI mechanism will slap him with a ClassCastException. Casting an object doesn't change its type; it merely changes the view of its type. An instance of SomeClassProxy remains SomeClassProxy no matter how it is cast. Now that both of the proxies and the implementation are using this new interface, you can create a factory that allows the user to ignore the actual implementation of the proxy itself. This factory is shown in Example 10-6. Example 10-6. A factory that gives back various proxiespackage oracle.hcj.proxies; public class SomeClassFactory { public static final SomeClass getSomeClassProxy( ) { SomeClassImpl impl = new SomeClassImpl(System.getProperty("user.name")); if (LOGGER.isDebugEnabled( )) { return new SomeClassCountingProxy(impl); } else { return new SomeClassProxy(impl); } } } The new factory method simply gives back an object of type SomeClass. Since both of the proxies implement SomeClass, you can give back whichever one you want. In this example, if Log4J is set for debugging, return the proxy that counts invocations; otherwise, return the noncounting proxy. To use the new factory method, the user simply needs to know he is getting an instance of SomeClass. Your new proxy scheme is shown here: package oracle.hcj.proxies; public class DemoInterfaceProxy { public static final void main(final String[] args) { SomeClass proxy = SomeClassFactory.getSomeClassProxy( ); proxy.someMethod( ); proxy.someOtherMethod("Our Proxy works!"); } } The user doesn't need to know (or care) if the proxy is counting. You can give him any proxy that conforms to the interface SomeClass. Using interfaces is clearly superior to using concrete classes because it decouples the client from the server object, allowing the developer to insert code between the client and server objects depending on various criteria. In this example, the count of the method invocations in the demo program is not shown. You could, however, check to see whether proxy is an instance of SomeCountingProxy and output the count: if (proxy instanceof SomeClassCountingProxy) { System.out.println(((SomeClassCountingProxy)proxy).getInvocationCount( )); } This code would allow you to print the count if the proxy is a counting proxy, or simply skip it if it is a noncounting proxy.
Similarly, you could create additional interfaces that extend the SomeClass interface to implement dynamically returned decorators, each adding new methods that are specific to the proxy classes. Each of the proxy classes would implement the extended interface, which would be used by the clients of the implementation. This is an extremely powerful way to deal with a variety of implementations. Dynamic Proxies
While coding proxies to objects, you may notice yourself doing the same thing over and over again in a proxy class. For example, for each implementation for which you wanted a counting proxy, you would have to replicate all of the work that you had already done for each new implementation. The problem with this is that all these proxy classes with duplicated code are troublesome to maintain. If your proxy had 1,000 lines of networking code, and a bug was found in just one line, you would have to remember to change that line in each bit of duplicated code, throughout tens, or even hundreds, of additional proxies.
To alleviate this problem, you could create helper utility methods that each proxy could call, but you would still be stuck with multiple proxy classes that do essentially the same thing. What you need is a way to implement the method-counting code in one location, and a way to reuse it for any object. Dynamic proxies can do this by using reflection. Dynamic proxies differ from static proxies in that they do not exist at compile time. Instead, they are generated at runtime by the JDK and then made available to the user. This allows the factory to bind an object to many different kinds of implementations without copying or duplicating code. Invocation handlersWhen writing a dynamic proxy, the principal task of the programmer is to write an object called an invocation handler, which implements the InvocationHandler interface from the java.lang.reflect package. An invocation handler intercepts calls to the implementation, performs some programming logic, and then passes on the request to the implementation. Once the implementation method returns, the invocation handler returns its results (along with any exceptions thrown). Example 10-7 shows an invocation handler that implements your method-invocation counting. Example 10-7. A method-invocation-counting invocation handlerpackage oracle.hcj.proxies; public class MethodCountingHandler implements InvocationHandler { private final Object impl; private int invocationCount = 0; public MethodCountingHandler(final Object impl) { this.impl = impl; } public int getInvocationCount( ) { return invocationCount; } public Object invoke(Object proxy, Method meth, Object[] args) throws Throwable { try { this.invocationCount++; Object result = meth.invoke(impl, args); return result; } catch (final InvocationTargetException ex) { throw ex.getTargetException( ); } } } This invocation handler provides the same functionality as static proxies. However, it uses reflection to do the job. When a user executes a method on the proxy, the invocation handler is called instead of the implementation. Inside the invocation handler, insert code to increment the invocationCount variable and then forward the call to the implementation using the invoke( ) method on the Method object. Once the invocation is complete, the implementation will return a value to the handler. You then pass that value back to the caller.
The code inside the invoke( ) method can do a variety of things. In this example, you simply count the invocations of methods. However, you could write an invocation handler that would perform security checks or even implement IIOP protocol to send method calls across the network. Generated proxy classesWriting an invocation handler is only the first step in generating a dynamic proxy. Once you have an invocation handler, you must generate a proxy for the users. Furthermore, according to the proxy design pattern, you have to make sure that the proxy looks like the implementation; the user shouldn't be aware of the differences between the proxy and the implementation. You can do this by using a java.lang.reflect.Proxy class in conjunction with your proxy factory. The resulting factory method is shown here: package oracle.hcj.proxies; public class SomeClassFactory { public static final SomeClass getDynamicSomeClassProxy( ) { SomeClassImpl impl = new SomeClassImpl(System.getProperty("user.name")); InvocationHandler handler = new MethodCountingHandler(impl); Class[] interfaces = new Class[] { SomeClass.class }; ClassLoader loader = SomeClassFactory.class.getClassLoader( ); SomeClass proxy = (SomeClass)Proxy.newProxyInstance(loader, interfaces, handler); return proxy; } } In this version of the factory method, SomeClass is an interface implemented by the actual implementation, named SomeClassImpl. This allows you to tell the Proxy class to generate a new proxy that implements the interface SomeClass and uses the invocation handler.
The Proxy class plays a pivotal role in creating and managing new proxy classes in the virtual machine. Proxy got its enormous power from only four methods:
Generated proxy classes are required to conform to the following rules:
There are several additional aspects of generated proxy classes of which you should be aware:
Using dynamic proxiesNow that you have your dynamic proxy class, you can use it in a new method: package oracle.hcj.proxies; public class DemoDynamicProxy { public static final void main(final String[] args) { SomeClass proxy = SomeClassFactory.getDynamicSomeClassProxy( ); proxy.someMethod( ); proxy.someOtherMethod("Our Proxy works!"); } } Using the proxy class is virtually identical to using the static proxy classes. However, there are a couple of important differences. First, the implementation of the proxy is generated by the Proxy class. Second, to get the method invocation count of the proxy, you would have to fetch the invocation handler and then ask the invocation handler for the count: InvocationHandler handler = Proxy.getInvocationHandler(proxy); if (handler instanceof MethodCountingHandler) { System.out.println(((MethodCountingHandler)handler).getInvocationCount( )); } The method Proxy.getInvocationHandler( ) is used to fetch the invocation handler from the proxy. You then make sure that the handler is in fact a MethodCountingHandler; remember that any invocation handler could have been used. Finally, output the invocation count. |
Previous Next |