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.

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