Vita Rara: A Life Uncommon

What's Wrong with Java's Dynamic Dispatch or "How I Implemented sendMessage()"


Categories:

(Related article using this technique to "script" some Java objects: article.)

The following code quickly illustrates an issue with the Reflection API's in the Java language. At run time finding methods on classes requires that the types passed to Class#findMethod() exactly match those found in the method declaration. The JavaDoc and language spec refers to these as the "formal parameter types".

The issue is, I have a method that takes an A, and I have an object of B that extends A. If you run the following code it will fail, being unable to find the method.

import java.lang.reflect.*;

public class ReflectionTest {
  public static void main (String[] args) {
    try {
      C c = new C ();
      Method m = c.getClass().getMethod ("execute", new Class[] { B.class });
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
public class A {
	
}

public class B extends A {
	
}

public class C {
  public void execute (A a) {
    System.out.println ("Got it.");
  }
}

This is the difference between determining the method call at run time and compile time. At compile time you can pass B to the method that takes A and all is well. At runtime Class#getMethod(String name, Class[] parameterTypes) will not find it.

I need this sort of functionality for a major EJB 2.1 to JPA conversion I'm working on. It's that or write thousands of lines of boilerplate code. *ick* So, this is the solution I've come up with:

import java.lang.reflect.*;
import java.util.*;

public class ReflectionTest {
  public static void main (String[] args) {
    try {
      A a = new A ();
      B b = new B ();
      C c = new C ();
      sendMessage ("execute", c, new Object[] { b });
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static Object sendMessage (String message, Object target, Object[] args) {
    try {
      // Is this an argumentless method call?
      if (args == null) {
        // Get the method.
        return target.getClass().getMethod (message, null).invoke (target, null);
      } else {

        // Get all methods from the target.
        Method[] allMethods = target.getClass().getMethods();
        List candidateMethods = new ArrayList ();

        for (int i = 0; i < allMethods.length; i++) {
          // Filter methods by name and length of arguments.
          Method m = allMethods[i];
          if (m.getName().equals (message) && m.getParameterTypes().length == args.length) {
            candidateMethods.add (m);
          }
        }

        if (candidateMethods.size() == 0) {
          throw new RuntimeException ("");
        }

        Method callableMethod = null;
        for (Iterator itr = candidateMethods.iterator(); itr.hasNext (); ) {
          boolean callable = true;
          Method m = (Method) itr.next ();
          Class[] argFormalTypes = m.getParameterTypes ();
          for (int i = 0; i < argFormalTypes.length; i++) {
            if (! argFormalTypes[i].isAssignableFrom ( args[i].getClass() )) {
              callable = false;
            }
          }
          if (callable) {
            callableMethod = m;
          }
        }

        if (callableMethod != null) {
          return callableMethod.invoke (target, args);
        } else {
          throw new RuntimeException ("No such method found: " + message);
        }
      }
    } catch (Exception e) {
      StringBuffer sb = new StringBuffer ();
      // Build a helpful message to debug reflection issues.
      try {
        sb.append ("ERROR: Could not send message '" + message + "' to target of type " + target.getClass().toString () + " \n");

        sb.append ("\ttarget implements : \n");
        Class[] interfaces = target.getClass().getInterfaces();
        for (int j = 0; j < interfaces.length; j++) {
          sb.append ("\t\t" + interfaces[j].getName () + "\n");
        }
        sb.append ("\n");

        sb.append ("\ttarget methods: \n");
        Method[] methods = target.getClass().getMethods();
        for (int j = 0; j < methods.length; j++) {
          sb.append ("\t\t" + methods[j].getName () + "\n");
        }
        sb.append ("\n");

        if (args != null) {
          sb.append ("\tArgument types: \n");
          for (int j = 0; j < args.length; j++) {
            sb.append ("\t\t" + args[j].getClass().getName() + "\n");
          }
        }
      } catch (Exception e2) {
        throw new RuntimeException ("ERROR: Could not create detailed error message for failed sendMessage() call.");
      }
      throw new RuntimeException (sb.toString() );
    }

  }
}

The first thing I determine is if there are any arguments. If there are no arguments we can just short circuit to a very simple method for finding the method to call by passing null for the Class[] of parameter types.

If there are parameters we have to find an appropriate method to call. The algorithm I adopted is based on a paper by Bela Ban from Cornell. I simplified it for my application from Bela's paper to the following:

  • If there are no arguments short circuit and try to find the method by name passing null for the parameter types.
  • If there is one or more arguments:
    • Get all of the methods on the class.
    • Filter the methods finding those with a matching name and number of arguments.
    • Filter these remaining methods and see if the types of our arguments are assignable to the types of the expected arguments. Pick one of the matching methods.

This method will match the method by name, number of parameters, and then test to see if the type of the args are assignable from the expected argument types. If a method is found it is invoked dynamically passing it the arguments. This combined with some basic conventions has allowed me to "script" hundreds of objects in my EJB 2.1 to JPA conversion eliminating a few thousand lines of boilerplate code.

As noted in Bela's paper my solution isn't completely correct, but for my application, due to our conventions it will work. The better method would be to work out the distances that the implementations are away from the expected arguments and pick the correct one using the same method the compiler does. Maybe at some point in the future I'll do that. But for today my code works and I've saved thousands of lines of typing.

Calling the most specific method

Hello,

I've modified the code to call the most specific method.

Cheers
~cdfh

package org.cdfh.misc;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/*
 * Original credit to Mark Menard:
 * http://www.vitarara.org/cms/whats_wrong_with_javas_dynamic_dispatch_or_how_i_implemented_sendMessage
 * Modified by cdfh to call the most specific method
 */
public class DynDispatch {

    /**
     * Dynamically dispatch message to target.
     * Dispatches to the most specific target.
     */
    public static Object sendMessage(String message, Object target, Object... args) throws Throwable {
        try {
            // Is this an argumentless method call?
            if (args == null) {
                // Get the method.
                return target.getClass().getMethod(message).invoke(target);
            } else {

                // Get all methods from the target.
                Method[] allMethods = target.getClass().getMethods();
                List<Method> callables = new LinkedList();

                NEXT:
                for (int i = 0; i < allMethods.length; i++) {
                    // Filter methods by name and length of arguments.
                    Method m = allMethods[i];
                    if (m.getName().equals(message) && m.getParameterTypes().length == args.length) {

                        for (int j = 0; j < m.getParameterTypes().length; j++) {
                            if (!m.getParameterTypes()[j].isAssignableFrom(args[j].getClass())) {
                                continue NEXT;
                            }
                        }

                        callables.add(m);
                    }
                }

                Collections.sort(callables, new Comparator<Method>() {

                    @Override
                    public int compare(Method a, Method b) {
                        return methodMoreSpecific(a, b);
                    }
                });

                if (callables.isEmpty()) {
                    throw new RuntimeException("No method found for: " + message);
                } else {
                    return callables.iterator().next().invoke(target, args);
                }
            }
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException(ex);
        } catch (InvocationTargetException ex) {
            throw ex.getCause();
        } catch (NoSuchMethodException ex) {
            throw new RuntimeException(ex);
        } catch (SecurityException ex) {
            throw new RuntimeException(ex);
        }

    }

    private final static int methodMoreSpecific(Method methodA, Method methodB) {
        Class[] argsA = methodA.getParameterTypes();
        Class[] argsB = methodB.getParameterTypes();
        for (int i = 0; i < argsA.length; i++) {
            if (argsA[i].isAssignableFrom(argsB[i])) {
                return 1;
            }
        }
        return -1;
    }

    static interface Foo {}
    static interface FooIChild extends Foo {}
    static class FooChild implements Foo {}
    static class FooChild1 extends FooChild {}
    static class FooChild2 extends FooChild {}

    public static void main(String args[]) {
        try {
            class Test {

                public void foo(String expected, Foo o) {
                    System.out.println("Foo: [" + expected + "] " + o.getClass());
                }

                public void foo(String expected, FooIChild o) {
                    System.out.println("FooIChild: [" + expected + "] " + o.getClass());
                }

                public void foo(String expected, FooChild o) {
                    System.out.println("FooChild: [" + expected + "] " + o.getClass());
                }

                public void foo(String expected, FooChild1 o) {
                    System.out.println("FooChild1: [" + expected + "] " + o.getClass());
                }

                public void foo(String expected, FooChild2 o) {
                    System.out.println("FooChild2: [" + expected + "] " + o.getClass());
                }
            }
            Test t = new Test();
            sendMessage("foo", t, "Foo", new Foo() {});
            sendMessage("foo", t, "FooIChild", new FooIChild() {});
            sendMessage("foo", t, "FooChild", new FooChild());
            sendMessage("foo", t, "FooChild1", new FooChild1());
            sendMessage("foo", t, "FooChild2", new FooChild2());
        } catch (Throwable ex) {
            Logger.getLogger(DynDispatch.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Sounds Interesting

I will take a look at this. It could be useful for me.

Thanks,

Mark

ayup.

I explored a similar solution a while back in an article from the old "Java Report" mag, entitled "Limitations of Reflective Method Lookup". You're welcome to use the code as-is if you like. Perhaps I'll update it for Java 5's generified Class, Method, Constructor, etc. constructs.

ayup.

http://www.adtmag.com/java/article.aspx?id=4276