Exposing an interface at runtime using a proxy class
Requires BCEL.
UPDATED: does verification at bridge generation time.
expose(Object, Class) will search for the correct public target type to use.
UPDATED: performance hacks.
Example usage:
public class BridgeTest { public interface CharList { public int length(); public char charAt(int i); } void print(CharList l) { for(int i=0; i<l.length(); i++) System.out.print(l.charAt(i)); } public static void main(String args[]) { CharList list = Bridge.expose("Hello world\n", CharList.class); print(list); } }
Bridge.java:
/* Make an object expose an interface at runtime */ import org.apache.bcel.generic.*; import org.apache.bcel.Constants; import java.lang.reflect.Method; import java.util.*; public abstract class Bridge<C,I> { protected C __target; // the implementation of the interface private final void __init(Object target) { this.__target = (C)target; } // cache already-generated bridges private static HashMap<Class, HashMap<Class, Class>> cache = new HashMap<Class, HashMap<Class, Class>>(); // allow injection of classes from byte arrays private static InjectingClassLoader loader = new InjectingClassLoader(); private static final void cacheSet(Class key1, Class key2, Class value) { HashMap<Class,Class> intermediate = cache.get(key1); if(intermediate == null) cache.put(key1, intermediate = new HashMap<Class,Class>()); intermediate.put(key2, value); } private static final Class cacheGet(Class key1, Class key2) { HashMap<Class,Class> intermediate = cache.get(key1); if(intermediate == null) return null; return intermediate.get(key2); } // returns [ [ifaceMethods...] [fromMethods...] ] private static Method[][] getMapping(Class from, Class iface) throws NoSuchMethodException,IllegalAccessException { if(!iface.isInterface()) throw new IllegalArgumentException(iface.getName()+" is not an interface"); if(!java.lang.reflect.Modifier.isPublic(from.getModifiers())) throw new IllegalAccessException(from.getName()+" is not public"); java.lang.reflect.Method[][] map = new java.lang.reflect.Method[2][]; map[0] = iface.getMethods(); map[1] = new java.lang.reflect.Method[map[0].length]; for(int i=0;i<map[0].length;i++) { try { Method match = from.getMethod(map[0][i].getName(), map[0][i].getParameterTypes()); if(!map[0][i].getReturnType().isAssignableFrom(match.getReturnType())) throw new NoSuchMethodException("Return type "+match.getReturnType().getName()+ " of "+toString(match)+" is not compatible with return type "+ map[0][i].getReturnType().getName()+" of "+toString(map[0][i])); map[1][i] = match; } catch (NoSuchMethodException e) { throw new NoSuchMethodException("Couldn't find "+toString(map[0][i])+" in "+ from.getName()); } } return map; } private static String toString(Method m) { StringBuffer sb = new StringBuffer(m.getDeclaringClass().getName()); sb.append(".").append(m.getName()).append("("); boolean first=true; for(Class c : m.getParameterTypes()) { if(!first) sb.append(", "); sb.append(c.getName()); first=false; } sb.append(")"); return sb.toString(); } private static <C,I> Class<? extends Bridge<C,I>> create(Class<C> from, Class<I> iface) { try { Method[][] map = getMapping(from, iface); String bridgeName = "Bridge_"+from.getSimpleName()+"_"+iface.getSimpleName(); // public class Bridge_Thing_Mungible extends Bridge<Thing,Mungible> implements Mungible ClassGen cg = new ClassGen( bridgeName, Bridge.class.getName(), // superclass name "<generated>", // source file name Constants.ACC_PUBLIC | Constants.ACC_FINAL | Constants.ACC_SUPER, // flags new String [] { iface.getName() } // interfaces ); ConstantPoolGen cpg = cg.getConstantPool(); InstructionFactory factory = new InstructionFactory(cg, cpg); ReferenceType targetType = (ReferenceType)Type.getType(from); for(int i=0; i<map[0].length; i++) { Method imeth = map[0][i]; Method fmeth = map[1][i]; Type ireturnType = Type.getType(imeth.getReturnType()), freturnType = Type.getType(fmeth.getReturnType()); Class[] iargs = imeth.getParameterTypes(), fargs = fmeth.getParameterTypes(); Type[] iargsT = new Type[iargs.length], fargsT = new Type[fargs.length]; String[] argNames = new String[iargs.length]; for(int j=0; j<iargs.length; j++) { iargsT[j] = Type.getType(iargs[j]); fargsT[j] = Type.getType(fargs[j]); argNames[j] = "arg"+i; } InstructionList il = new InstructionList(); // public int munge(Object arg0, int arg1) { MethodGen mg = new MethodGen( Constants.ACC_PUBLIC, ireturnType, iargsT, argNames, imeth.getName(), bridgeName, il, cpg); // (Thing)this.__target il.append(new ALOAD(0)); il.append(factory.createFieldAccess( Bridge.class.getName(), "__target", Type.OBJECT, Constants.GETFIELD)); il.append(factory.createCheckCast(targetType)); // .munge(arg0, arg1); int position = 1; for(int j=0;j<iargs.length;j++) { il.append(factory.createLoad(iargsT[j], position)); position += iargsT[j].getSize(); } il.append(factory.createInvoke( from.getName(), fmeth.getName(), freturnType, fargsT, from.isInterface() ? Constants.INVOKEINTERFACE : Constants.INVOKEVIRTUAL)); // return (last result, if any) il.append(factory.createReturn(ireturnType)); mg.setMaxStack(); cg.addMethod(mg.getMethod()); il.dispose(); // Allow instruction handles to be reused } // public Bridge_Thing_Mungible() { super(); } cg.addEmptyConstructor(Constants.ACC_PUBLIC); try { cg.getJavaClass().dump("proxy.class"); } catch(Exception e) { throw new RuntimeException(e); } byte[] classData = cg.getJavaClass().getBytes(); Class c = loader.load(cg.getClassName(), classData); return (Class<Bridge<C,I>>)c; } catch (IllegalAccessException e) { throw new BridgeFailure(from, iface, e); } catch (NoSuchMethodException e) { throw new BridgeFailure(from, iface, e); } } private static <C,I> Class<? extends Bridge<C,I>> get(Class<C> from, Class<I> iface) { // Pair<Class,Class> key = new Pair<Class,Class>(from, iface); Class<? extends Bridge<C,I>> bridgeClass = (Class<? extends Bridge<C,I>>)cacheGet(from, iface); if(bridgeClass == null) { bridgeClass = create(from, iface); cacheSet(from, iface, bridgeClass); } return bridgeClass; } // Expose interface iface by creating a proxy. // the type of target must be from, a subclass of from, or a class implementing from. // from must be public, and must expose all methods in iface. public static <I> I expose(Object target, Class from, Class<I> iface) { try { Class<? extends Bridge<?,I>> c = Bridge.get(from, iface); Bridge<?,I> proxy = c.newInstance(); proxy.__init(target); return (I)proxy; } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } // Expose interface iface by creating a proxy. // Tries expose(target, target.getClass(), iface) first. // then works up the class hierarchy. If doesn't find a public superclass // that exposes all methods, it tries interfaces. // if you know in advance the class or interface that exposes the needed methods, // use expose(target, Class.forName("ExposingClass"), iface). public static <I> I expose(Object target, Class<I> iface) { Class tClass = target.getClass(); Class from = tClass; do { try { I result = expose(target, from, iface); if(tClass != from) // if tClass == tFrom then get() sets the cache cacheSet(tClass, iface, result.getClass()); return result; } catch (BridgeFailure e) { Throwable cause = e.getCause(); if(cause instanceof NoSuchMethodException) { // have traced superclass up until the interface isn't satisfied. // try interfaces and then give up. // in the case of a private implementation of a public interface, // this allows you to subset the interface. Class[] targetIfaces = target.getClass().getInterfaces(); for(Class targetIface : targetIfaces) try { I result = expose(target, targetIface, iface); cacheSet(tClass, iface, result.getClass()); // perf hack return result; // ignore, reported exception is that for class hierarchy. } catch (BridgeFailure ex) {} throw new BridgeFailure(target, iface, e); } else if(!(cause instanceof IllegalAccessException)) throw e; } from = from.getSuperclass(); } while(from != null); throw new RuntimeException("Object "+target+" ("+target.getClass().getName()+") has no public superclass"); } } class BridgeFailure extends RuntimeException { public BridgeFailure(Class from, Class iface, Exception cause) { super("Could not map class "+from.getName()+" to interface "+iface.getName()); initCause(cause); } public BridgeFailure(Object target, Class iface, Exception cause) { super("Could not map first public supertype of " + target + " (" + target.getClass().getName() + ")," + " or any implemented interface, to interface " + iface.getName()); initCause(cause); } } // ClassLoader implementation to let you load classes from byte arrays class InjectingClassLoader extends ClassLoader { public InjectingClassLoader() { super(InjectingClassLoader.class.getClassLoader()); } public Class load(String name, byte[] buffer) { return defineClass(name, buffer, 0, buffer.length); } }