Monday, July 16, 2007

Dealing with method missing with Groovy's MetaClass system

One of the new features coming up in Groovy 1.1 to be released later this year is ExpandoMetaClass. Its an elegant API to programatically extend a class' functionality with Groovy's Meta Object Protocol (MOP).

An example of how we take advantage of this is Grails' dynamic finders in GORM. Dynamic finders allow you to do things like Book.findByTitleAndAuthor("It", "Stephen King"). So how are these implemented in Grails?

First we defined an interface that allows the matching of a method signature to a given method pattern. That interface goes something like this:

interface StaticMethodInvocation {
boolean isMethodMatch(String methodName)
Object invoke(Class theClass, String methodName, Object[] args)
}

The implementation of this interface just uses regular expressions to match the method signature. Simple enough. So how do we use this from the MetaClass itself? Well, this is where the magic of ExpandoMetaClass comes in as it allows us to override a static "invokeMethod" in Groovy:

1 def dynamicMethods = [...]
2 Book.metaClass.'static'.invokeMethod = { String methodName, args ->
3 def metaMethod = Book.metaClass.getStaticMetaMethod(methodName, args)
4 def result
5 if(metaMethod) {
6 result = metaMethod.invoke(dc.clazz, args)
7 }
8 else {
9 StaticMethodInvocation method =
10 dynamicMethods.find { it.isMethodMatch(methodName) }
11 if(method) {
12 Book.metaClass.'static'."$methodName" = { Object[] varArgs ->
13 method.invoke(Book.class, methodName, varArgs)
14 }
15 result = method.invoke(Book.class, methodName, args)
16 }
17 else {
18 throw new MissingMethodException(methodName, Book.class, args)
19 }
20 }
21 result
22 }

So what is actually happening here? First we look to see if the method already exists
on the line:

3 def metaMethod = Book.metaClass.getStaticMetaMethod(methodName, args)

If it does we simply invoke the method:

6 result = metaMethod.invoke(dc.clazz, args)

Otherwise we attempt to find a method that matches the method signature using a previously defined list of StaticMethodInvocation instances:

9 StaticMethodInvocation method =
10 dynamicMethods.find { it.isMethodMatch(methodName) }

If the method exists we dynamically register a new method on the MetaClass so that the next time the method is invoked it doesn't have to go through the matching process and will simply dispatch like normal:

12 Book.metaClass.'static'."$methodName" = { Object[] varArgs ->
13 method.invoke(dc.clazz, methodName, varArgs)
14 }

Finally, we invoke the method itself, or if the method isn't matched we throw a MethodMissingException:

15 result = method.invoke(dc.clazz, methodName, args)

Job done. As simple as that ;-)

I'm doing a talk at the Grails eXchange about the Grails plug-in system and how we dynamically extend the behaviour of classes. See you there!

1 comment:

Anonymous said...

Would be interesting to know how Dynamically methods could be added to Grails Domain classes