DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Snippets has posted 5883 posts at DZone. View Full User Profile

Exposing An Interface At Runtime Using A Proxy Class

11.20.2007
| 9052 views |
  • submit to reddit
        Force an object expose an interface at runtime, through a generated proxy class. It must implement all required methods.
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);  
	}  
}