package org.antforge; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.NotFoundException; /** * A proxy factory supports the rapid development of a transportation * layer based on RMI for given clean interfaces. The interface classes * do not need to be "cluttered" with specifics of the transportation * implementation. This means no more annoying appearances of classes * like java.rmi.Remote or java.rmi.RemotException in your interfaces. *
* Interfaces can explicitly be marked as "remote interfaces". All methods * exposed by such an interface can be invoked from a remote site. All * parameters which are subclasses of such interfaces, will be passed * by-reference and will not be serialized. This is just the same behavior * as if the interface would extend java.rmi.Remote in the RMI world. * The actual remote interfaces are generated on-demand at runtime. *
* A proxy is a transparent object implementing both, the local and the * generated remote interface. Both interfaces are usable: *
    *
  1. Cast the proxy to java.rmi.Remote and use it with any naming or * registry service available to the RMI world.
  2. *
  3. Cast the proxy to your local interface and don't bother whether * it actually targets a local or a remote site.
  4. *
* The decision how an invocation actually is dispatched can be solely * based on whether the target object of a proxy is a remote or a local * one. This decision is hidden inside the transportation layer. *
* This is my attempt to show how RMI should have been designed in the * first place. Please feel free to comment, improve, ignore or flame. * * @author Michael Starzinger * @version 0.1-20100406 */ public class ProxyFactory { private static ProxyFactory singleton; private List> markedInterfaces; private Map, String> generatedNames; private Map, Class> generatedInterfaces; private Map createdProxies; private ProxyFactory() { // XXX We probably should use weak references here! super(); this.markedInterfaces = new ArrayList>(); this.generatedNames = new HashMap, String>(); this.generatedInterfaces = new HashMap, Class>(); this.createdProxies = new HashMap(); } /** * At the moment we only allow one proxy factory per VM because classes * are generated during runtime and we have to ensure they are unique. * To resolve this issue properly, move interface generation into a * singleton and allow multiple proxy factories. Also remember that * this factory is not at all thread-safe. * @return The proxy factory singleton. */ public static ProxyFactory getFactory() { if (singleton == null) singleton = new ProxyFactory(); return singleton; } /** * Marks the given interface as being a "remote interface". * @param localInterface A plain-old interface class. */ public void markAsRemote(Class localInterface) { // Check if given class really is an interface. if (!localInterface.isInterface()) throw new ProxyException("Given class is not an interface."); // Check if given interface is a remote interface. if (Remote.class.isAssignableFrom(localInterface)) throw new ProxyException("Given interface already is a remote interface."); // Check if given interface is already marked as remote. if (markedInterfaces.contains(localInterface)) return; // Mark given interface as remote. markedInterfaces.add(localInterface); } private String getRemoteInterfaceName(Class localInterface) { // Check if name for remote interface already generated. if (generatedNames.containsKey(localInterface)) return generatedNames.get(localInterface); // Otherwise trigger interface generation. Class remoteInterface = getRemoteInterface(localInterface); return remoteInterface.getName(); } private Class getRemoteInterface(Class localInterface) { // Check if remote interface already generated. if (generatedInterfaces.containsKey(localInterface)) return generatedInterfaces.get(localInterface); // Generate a new remote interface. Class remoteInterface; try { remoteInterface = generateRemoteInterface(localInterface); } catch (NotFoundException e) { throw new ProxyException(e); } catch (CannotCompileException e) { e.printStackTrace(); throw new ProxyException(e); } // Remember the newly generated remote interface and return it. generatedInterfaces.put(localInterface, remoteInterface); return remoteInterface; } private Class getOriginalInterface(Class remoteInterface) { // Check if remote interface was generated before. for (Class localInterface : generatedInterfaces.keySet()) if (generatedInterfaces.get(localInterface).equals(remoteInterface)) return localInterface; // If we did not generate the remote interface, something is wrong. throw new ProxyException("Unable to find original interface type."); } private int uniqueCounter = 1; private String generateUniqueName() { return "$Dynamic" + uniqueCounter++; } private Class generateRemoteInterface(Class localInterface) throws NotFoundException, CannotCompileException { ClassPool pool = ClassPool.getDefault(); // Get all the RMI specific classes. CtClass ccRemote = pool.get("java.rmi.Remote"); CtClass ccRemoteException = pool.get("java.rmi.RemoteException"); // Generate a new unique name for the remote interface and remember it // separately to prevent infinite loops. String name = generateUniqueName(); generatedNames.put(localInterface, name); // Create a remote interface which is a subclass of java.rmi.Remote just // like RMI wants. CtClass cc = pool.makeInterface(name, ccRemote); // Iterate over all methods of the interface. for (Method method : localInterface.getDeclaredMethods()) { // Replace all parameter types which are subclasses of an interface // marked as being remote by their generated counterparts so that they // can be passed by reference. // XXX Subclass relationships are currently ignored. ArrayList parameters = new ArrayList(); for (Class parameterType : method.getParameterTypes()) { if (markedInterfaces.contains(parameterType)) parameters.add(pool.get(getRemoteInterfaceName(parameterType))); else parameters.add(pool.get(parameterType.getName())); } // Create a new method with the above parameters and make the method throw // a java.rmi.RemoteException just like RMI wants. CtMethod cm = CtNewMethod.abstractMethod( pool.get(method.getReturnType().getName()), method.getName(), parameters.toArray(new CtClass[0]), new CtClass[] { ccRemoteException }, cc); // Add method to the newly created interface. cc.addMethod(cm); } // Actually create and load the generated remote interface. Class remoteInterface = cc.toClass(); //System.out.println("GENERATED REMOTE INTERFACE"); //System.out.println(" LOCAL-TYPE: " + localInterface); //System.out.println(" REMOTE-TYPE: " + remoteInterface); return remoteInterface; } /** * Returns a transparent proxy object implementing both, the local and * the generated remote interface. * @param object The target object which will receive invocations. * @param localInterface A plain-old interface class which has been * marked as being remote before. * @return The transparent proxy object. */ public Object getProxy(Object object, Class localInterface) { // Check if there already is a proxy for the given object. if (createdProxies.containsKey(object)) return createdProxies.get(object); // Check if the target object itself already is a proxy. if (createdProxies.containsValue(object)) throw new ProxyException("Given object already is a transparent proxy."); // Check if the given interface is marked as remote. if (!markedInterfaces.contains(localInterface)) throw new ProxyException("Given interface is not marked as remote."); // The given object must either implement the local or the // generated remote interface. Class remoteInterface = getRemoteInterface(localInterface); if (!localInterface.isInstance(object) && !remoteInterface.isInstance(object)) throw new ProxyException("Given object does not implement the interface."); // Create a new proxy implementing both of the given interfaces. // XXX Users might want to proxify several interfaces at once. Object proxy = Proxy.newProxyInstance( object.getClass().getClassLoader(), new Class[] { localInterface, remoteInterface }, new ProxyInvocationHandler(object)); //System.out.println("GENERATED PROXY FOR " + object.getClass()); //System.out.println(" LOCAL-TYPE: " + localInterface); //System.out.println(" REMOTE-TYPE: " + remoteInterface); // Export the created proxy as a remote object. This is necessary // because proxies will be transported by-reference and thus need // to be reachable from a remote site. try { UnicastRemoteObject.exportObject((Remote) proxy, 0); } catch (RemoteException e) { throw new ProxyException("Unable to export created proxy"); } // Remember the proxy and return it. createdProxies.put(object, proxy); return proxy; } /** * All invocations received by a proxy will be dispatched by this * invocation handler. */ private class ProxyInvocationHandler implements InvocationHandler { private final Object target; public ProxyInvocationHandler(Object target) { super(); this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Class[] parameterTypes = method.getParameterTypes(); // We need to assemble a new list of parameter types and // arguments for the target object. List targetArguments = new ArrayList(); List> targetParameterTypes = new ArrayList>(); // Map parameter types and arguments for target method by // iterating over all parameters. for (int i = 0; i < parameterTypes.length; i++) { Object argument = args[i]; Class parameterType = parameterTypes[i]; // If the target is a remote object and the parameter is a // subclass of an interface marked as remote, we need to map // to the generated remote interface. // XXX Subclass relationships are currently ignored. if (Remote.class.isInstance(target) && markedInterfaces.contains(parameterType)) { targetArguments.add(getProxy(argument, parameterType)); targetParameterTypes.add(getRemoteInterface(parameterType)); continue; } // If the target is a local object and the parameter is a // generated remote interface, we need to map back to the // original local interface. if (!Remote.class.isInstance(target) && generatedInterfaces.containsValue(parameterType)) { Class localInterface = getOriginalInterface(parameterType); targetArguments.add(getProxy(argument, localInterface)); targetParameterTypes.add(localInterface); continue; } // The default behavior is to just pass the argument without // any mapping. targetArguments.add(argument); targetParameterTypes.add(parameterType); } //System.out.println("PERFORMING INVOCATION INSIDE PROXY"); //System.out.println(" INTERFACE-METHOD: " + method); // Find the target method. Method targetMethod = target.getClass().getMethod( method.getName(), targetParameterTypes.toArray(new Class[0])); //System.out.println(" TARGET-METHOD: " + targetMethod); // Actually invoke the target method. // XXX We probably need to proxify the returned object. Object result = targetMethod.invoke(target, targetArguments.toArray()); return result; } } /** * All errors happening inside the transport layer will be passed out * using this exception type. Since it is an unchecked exception we * ensure "clutter-free" code. */ private static class ProxyException extends RuntimeException { private static final long serialVersionUID = 1L; public ProxyException(String message) { super(message); } public ProxyException(Throwable cause) { super(cause); } } }