Thursday, September 08, 2005
Dynamic delegation, act 2
Last time, I showed an example of an InvocationHandler that used reflection on its own methods to decide when and how to delegate. Today I am going to take this a step further and present a base class that can be extended for any number uses. All of the code in this post has been tested, and I may also make it available in a zip file.
Firstly, the main class, DynamicDelegator. This is an abstract class that needs to be extended to add the methods that you want to override. For a simple example, a wrapper around java.util.Map might look like this:
Another class might use MapWrapper like this:
When
Here is an example that illustrates the introduction of an extra interface (in this case, Runnable) that is not implemented by the wrapped class:
Obviiously the above is highly contrived. The code to use it might look like this:
What follows is the actual code for DynamicDelegator and the supporting classes that make it work. (HashKey and MethodSig from last time are used as well, but are not repeated).
Now, to prove that I tested this thing, here is a JUnit test case which creates a couple of concrete implementations:
You will no doubt have noticed that DyanamicDelegator uses another class called InvocationChain to do the heavy lifting. InvocationChain is what I ended up with after re-factoring as much of logic related to conditional delegation out into it's own class, and generalizing it as much as possible.
InvocationChain is a pretty wild and wooly utility by itself. You could use it to combine unrelated classes and interfaces in a complete arbitrary way, as this test code illustrates:
Finally, InvocationHandler itself needs a method to enumerate all the interfaces implemented by a class and any of it's superclasses, and the super-interfaces of all of those, and so on. Because this is a general utility I put it into a separate class.
Next time: more examples of what you can do with this stuff.
Firstly, the main class, DynamicDelegator. This is an abstract class that needs to be extended to add the methods that you want to override. For a simple example, a wrapper around java.util.Map might look like this:
public class MapWrapper extends DynamicDelegator {
public MapWrapper(Object o) {
super(o);
}
public Object put(Object key, Object value) {
System.out.println("Put called!");
return ((Map)wrapped).put(key,value);
}
}
Another class might use MapWrapper like this:
Map m = (Map) new MapWrapper(new HashMap()).getProxy();
m.put("foo","bar");
System.out.println( m.get("foo"));
When
put
is called, the method declared in MapWrapper will be executed. When get
is called, the method executed will be the one from HashMap. It so happens that in this case, MapWrapper's put
method also calls the method on the underlying Map after doing some logging output.Here is an example that illustrates the introduction of an extra interface (in this case, Runnable) that is not implemented by the wrapped class:
public class MapWrapper extends DynamicDelegator implements Runnable {
public MapWrapper(Object o) {
super(o);
}
public Object put(Object key, Object value) {
System.out.println("Put called!");
return ((Map)wrapped).put(key,value);
}
public void run() {
System.out.println("Whoohoo!");
}
}
Obviiously the above is highly contrived. The code to use it might look like this:
Map m = (Map) new MapWrapper(new HashMap()).getProxy();
m.put("foo","bar");
System.out.println( m.get("foo"));
((Runnable)m).run();
What follows is the actual code for DynamicDelegator and the supporting classes that make it work. (HashKey and MethodSig from last time are used as well, but are not repeated).
/*
* DynamicDelegator.java
*
* Created on January 13, 2005, 9:31 AM
*/
import java.lang.reflect.*;
/**
* A class to make it easy to write dynamic proxies that
* wrap existing classes. Simply write methods that have
* the same names and signatures as the methods that you
* want to override. All other methods will be tunnelled
* straight through to the wrapped object. Uses reflection
* heavily, obviously.
* <p>
* It is not neccessary for the wrapper class to implement the same
* interface(s) as the wrapped object at compile time. Method name matching
* takes place dynamically at run time.
* <p>
* For a simple example, a wrapper around java.util.Map might look like this:
* <pre>
* public class MapWrapper extends DynamicDele {
*
* public MapWrapper(Object o) {
* super(o);
* }
*
* public Object put(Object key, Object value) {
* System.out.println("Put called!");
* return ((Map)wrapped).put(key,value);
* }
* }
* </pre>
* Another class might use MapWrapper like this:
* <pre>
* Map m = (Map) new MapWrapper(new HashMap()).getProxy();
* m.put("foo","bar");
* System.out.println( m.get("foo"));
* </pre>
* When <code>put</code> is called, the method declared in MapWrapper
* will be executed. When <code>get</code> is called, the method executed
* will be the one from HashMap. It so happens that in this case, MapWrapper's
* <code>put</code> method also calls the method on the underlying Map after
* doing some logging output.
* <p>
* Here is an example that illustrates the introduction of an extra interface
* (in this case, Runnable) that is not implemented by the wrapped class:
*<pre>
* public class MapWrapper extends DynamicDelegator implements Runnable {
*
* public MapWrapper(Object o) {
* super(o);
* }
*
* public Object put(Object key, Object value) {
* System.out.println("Put called!");
* return ((Map)wrapped).put(key,value);
* }
*
* public void run() {
* System.out.println("Whoohoo!");
* }
* }
* </pre>
* Obviiously the above is highly contrived.
* The code to use it might look like this:
* <pre>
* Map m = (Map) new MapWrapper(new HashMap()).getProxy();
* m.put("foo","bar");
* System.out.println( m.get("foo"));
* ((Runnable)m).run();
* </pre>
* @see java.lang.reflect.Proxy
* @see java.lang.reflect.InvocationHandler
* @see WrapperFactory
* @author jRobertson
*/
public abstract class DynamicDelegator {
/**
* The inner object that is being wrapped by us.
*/
protected Object wrapped;
/**
* If the getProxy method has been called, thisProxy
* is a reference to the proxy object that was returned
* by getProxy. This reference may be passed to any callbacks
* or other methods where you would normally pass "this".
*/
protected Object thisProxy;
/** Creates a new instance of DynamicDelegator */
public DynamicDelegator(Object wrapped) {
this.wrapped = wrapped;
}
/**
* Uses InvocationChain to tie this wrapper and the wrapped object
* together as a single proxy instance. The proxy implements all
* interfaces that are implemented by either this wrapper or by
* the wrapped Object, as well as any interfaces returned by the
* getAdditionalInterfaces method. Any method called on this proxy
* will be invoked on the wrapper if such a method if found there,
* or on the wrapped object otherwise.
* @see InvocationChain
*/
public Object getProxy() {
InvocationChain chain = new InvocationChain();
chain.add(this);
chain.add(wrapped);
chain.addInterfaces(getAdditionalInterfaces());
thisProxy = chain.newProxyInstance();
return thisProxy;
}
/**
* Normally this returns a zero-length array. Override to specify
* that the proxies created by this class should implement the
* returned interface(s) in addition to any that are implemented
* by the wrapper or the wrapped object.
*/
protected Class[] getAdditionalInterfaces() {
return new Class[0];
}
}
Now, to prove that I tested this thing, here is a JUnit test case which creates a couple of concrete implementations:
/*
* WrapperTest.java
* JUnit based test
*
* Created on January 13, 2005, 9:43 AM
*/
import junit.framework.*;
/**
*
* @author jRobertson
*/
public class WrapperTest extends TestCase {
public WrapperTest(String testName) {
super(testName);
}
protected void setUp() throws java.lang.Exception {
}
protected void tearDown() throws java.lang.Exception {
}
// TODO add test methods here. The name must begin with 'test'. For example:
// public void testHello() {}
public void testMap() throws Exception {
java.util.Map m = new java.util.HashMap();
MapWrapper mw = new MapWrapper(m);
java.util.Map proxyMap = (java.util.Map) mw.getProxy();
proxyMap.put("a","b");
assertEquals("b", proxyMap.get("a"));
assertTrue(mw.putCalled);
}
public void testRunnableMap() throws Exception {
java.util.Map m = new java.util.HashMap();
RunnableMapWrapper mw = new RunnableMapWrapper(m);
java.util.Map proxyMap = (java.util.Map) mw.getProxy();
proxyMap.put("a","b");
assertEquals("b", proxyMap.get("a"));
assertTrue(mw.putCalled);
((Runnable)proxyMap).run();
assertTrue(mw.runRan);
}
public void testRunnableMap2() throws Exception {
java.util.Map m = new java.util.HashMap();
RunnableMapWrapper2 mw = new RunnableMapWrapper2(m);
java.util.Map proxyMap = (java.util.Map) mw.getProxy();
proxyMap.put("a","b");
assertEquals("b", proxyMap.get("a"));
assertTrue(mw.putCalled);
((Runnable)proxyMap).run();
assertTrue(mw.runRan);
}
public class MapWrapper extends DynamicDelegator {
public boolean putCalled = false;
public MapWrapper(Object o) {
super(o);
}
public Object put(Object key, Object value) {
putCalled = true;
return ((java.util.Map)wrapped).put(key,value);
}
}
public class RunnableMapWrapper extends DynamicDelegator {
public boolean putCalled = false;
public boolean runRan = false;
public RunnableMapWrapper(Object o) {
super(o);
}
protected Class[] getAdditionalInterfaces() {
return new Class[] {
Runnable.class
};
}
public Object put(Object key, Object value) {
putCalled = true;
System.out.println("Put called!");
return ((java.util.Map)wrapped).put(key,value);
}
public void run() {
runRan = true;
}
}
public class RunnableMapWrapper2 extends DynamicDelegator implements Runnable {
public boolean putCalled = false;
public boolean runRan = false;
public RunnableMapWrapper2(Object o) {
super(o);
}
public Object put(Object key, Object value) {
putCalled = true;
System.out.println("Put called!");
return ((java.util.Map)wrapped).put(key,value);
}
public void run() {
runRan = true;
}
}
}
You will no doubt have noticed that DyanamicDelegator uses another class called InvocationChain to do the heavy lifting. InvocationChain is what I ended up with after re-factoring as much of logic related to conditional delegation out into it's own class, and generalizing it as much as possible.
/*
* InvocationChain.java
*
* Created on January 18, 2005, 10:46 PM
*/
import java.lang.reflect.*;
import java.util.*;
/**
* Uses reflection to create a chain of responsibility out of a
* collection of otherwise unrelated classes.
*
* @see java.lang.Proxy
* @see java.lang.InvocationHandler
* @see DynamicDelegator
*
* @author jRobertson
*/
public class InvocationChain implements InvocationHandler {
private Set interfaces = new HashSet();
private LinkedList links = new LinkedList();
/** Creates a new instance of InvocationChain */
public InvocationChain() {
}
/** Creates a new instance of InvocationChain
* with one object.
*/
public InvocationChain(Object a) {
this();
add(a);
}
/** Creates a new instance of InvocationChain
* with one object.
*/
public InvocationChain(Object a, Object b) {
this();
add(a);
add(b);
}
/**
* Adds the specified object to the end of the chain.
*/
public void add(Object o) {
ChainLink link = new ChainLink(o);
links.add(link);
ReflectionUtils.getAllInterfaces(o.getClass(), interfaces);
}
/**
* Manually add this interface to this list of interfaces
* that will be implemented by this chain.
*/
public void addInterface(Class c) {
interfaces.add(c);
}
/**
* Manually add these interfaces to this list of interfaces
* that will be implemented by this chain.
*/
public void addInterfaces(Class[] c) {
interfaces.addAll(Arrays.asList(c));
}
/**
* This method implements the InvocationHandler interface. The chain
* of responsibility will be searched until a method of the required
* name and argument types is found, and that method will then be
* invoked. The search stops after finding the first occurrence of
* a method in the chain, so the order in which the objects were
* added is important.
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// search the objects in the chain, one by one
for(Iterator i=links.iterator(); i.hasNext();) {
ChainLink link = (ChainLink) i.next();
// if the class implements the interface that
// declared this method, run it. we do this because
// I've seen cases where methods declared by a class's
// distant ancestors don't show up when you enumerate
// it's methods. specifically, the Connection objects
// returned from Oracle's database driver are like this.
if( method.getDeclaringClass().isAssignableFrom( link.getObject().getClass() ) ) {
return method.invoke(link.getObject(), args);
}
// if the class has a method that has the same
// name and type as this method, find that
// method and run it instead
Method match = link.matchMethod(method);
if( match != null) {
return match.invoke(link.getObject(), args);
}
}
// ok, the fast way of just looking stuff up in a hashmap
// failed... now try the slow way that involves catching
// exceptions. not sure if it is really possible to get
// here, but you never can tell with reflection
for(Iterator i=links.iterator(); i.hasNext();) {
ChainLink link = (ChainLink) i.next();
try {
return method.invoke(link.getObject(), args);
} catch (IllegalAccessException e) {
// nothing
} catch (IllegalArgumentException e) {
// nothing
}
}
throw new NoSuchMethodException("couldn't find metho in my chain of respsonsibility: "+method);
}
/**
* Returns the set of all interfaces implemented by
* any objects in the chain.
*/
public Class[] getInterfaces() {
Class[] intArray = new Class[interfaces.size()];
return (Class[]) interfaces.toArray(intArray);
}
/**
* Creates a proxy that implements all interfaces that are
* implemented by any object in the chain, and with this
* InvocationChain as the invocation handler.
*/
public Object newProxyInstance() {
return Proxy.newProxyInstance(getClass().getClassLoader(), getInterfaces(), this);
}
/**
* Inner class for each delegate in the chain.
*/
private static class ChainLink {
private Map methodMap = new java.util.HashMap();
private Object object;
ChainLink(Object o) {
object = o;
Method[] methods = o.getClass().getMethods();
for(int i=0; i<methods.length;i++) {
Method method = methods[i];
// do not count methods that we can't access
if( Modifier.isPublic(method.getModifiers())
&&
Modifier.isPublic(method.getDeclaringClass().getModifiers())
) {
methodMap.put( new MethodSig(method), method);
}
}
}
Method matchMethod(Method m) {
return (Method)methodMap.get( new MethodSig(m));
}
Object getObject(){ return object; }
}
}
InvocationChain is a pretty wild and wooly utility by itself. You could use it to combine unrelated classes and interfaces in a complete arbitrary way, as this test code illustrates:
import junit.framework.*;
/*
* InvocationChainTest.java
* JUnit based test
*
* Created on September 1, 2005, 12:02 PM
*/
/**
*
* @author jRobertson
*/
public class InvocationChainTest extends TestCase {
public InvocationChainTest(String testName) {
super(testName);
}
public static interface Animal {
String speak();
}
public static interface Vehicle {
int topSpeed();
}
public static class Dog implements Animal {
public String speak() {
return "woof!";
}
}
public static class Sled implements Vehicle {
public int topSpeed() {
return 5;
}
}
public void testChain() {
InvocationChain chain = new InvocationChain(new Dog(), new Sled());
Object dogSled = chain.newProxyInstance();
assertEquals( "woof!", ((Animal) dogSled).speak() );
assertEquals( 5, ((Vehicle) dogSled).topSpeed() );
}
}
Finally, InvocationHandler itself needs a method to enumerate all the interfaces implemented by a class and any of it's superclasses, and the super-interfaces of all of those, and so on. Because this is a general utility I put it into a separate class.
/*
* ReflectionUtils.java
*
* Created on January 18, 2005, 8:07 PM
*/
/**
* Utility methods for reflection, classes, interfaces, etc.
* @author jRobertson
*/
public class ReflectionUtils {
/**
* Recursively walks the graph of all superclasses and interfaces
* that are related to the passed-in class, and adds all the Class
* objects representing all the interfaces that are found to the
* passed-in Set.
*/
public static java.util.Set getAllInterfaces(Class c) {
java.util.Set s = new java.util.HashSet();
getAllInterfaces(c, s);
return s;
}
/**
* Recursively walks the graph of all superclasses and interfaces
* that are related to the passed-in class, and adds all the Class
* objects representing all the interfaces that are found to the
* passed-in Set.
*/
public static void getAllInterfaces(Class c, java.util.Set col) {
if(c == null) {
return;
}
Class[] ifaces = c.getInterfaces();
for(int i=0; i< ifaces.length; i++) {
col.add(ifaces[i]);
getAllInterfaces(ifaces[i], col);
}
getAllInterfaces(c.getSuperclass(), col);
}
}
Next time: more examples of what you can do with this stuff.