Meta Programming

The Groovy language supports two flavors of metaprogramming: runtime and compile-time. The first allows altering the class model and the behavior of a program at runtime while the second only occurs at compile-time, with runtime metaprogramming we can postpone to runtime the decision to intercept, inject and even synthesize methods of classes and interfaces.

Meta programming is a advanced feature in Groovy and if you only need to understand the basics (use with Jenkins for example) you don't really need this section.

Below are some terms that you need to know before we start looking at meta programming.

Meta Object Protocol (MOP)

The Meta Object Protocol (MOP) is a collection of rules of how a request method call is handled by the Groovy runtime system and how to control the intermediate layer. The MOP works like a filter for all method calls that originate from code that was compiled by Groovy, the MOP makes all objects appear rich and powerful.


Customizing the MOP

Know we understand that there is a MOP that intercepts a method, we can use hooks to intercept the method call

invokeMethod example
class InvokeDemo {

    // will get called if unable to find a method
    def invokeMethod(String name, Object args){
        "called invokeMethod $name $args"
    }

    def test(){
        "method exists"
    }
}

def invokeDemo = new InvokeDemo()
println invokeDemo.test()

// if method is not defined then it will try and find a invokeMethod
println invokeDemo.someMethod()
getProperty example
class PropertyDemo {

    def prop1 = "prop1"
    def prop2 = "prop2"
    def prop3 = "prop3"

    // will be invoked first before the normal getter method
    def getProperty(String name){
        println "getProperty() called with argument $name"

        if(metaClass.hasProperty(this, name)){
            metaClass.getProperty(this,name)
        } else {
            println "lets do something fun with this property"
            return "Party Time!!!!!"
        }
    }
}

def pd = new PropertyDemo()
println pd.prop1
println pd.prop2
println pd.prop3

// will be captured by the if statement
// really should use PropertyMissing method
println pd.prop4
propertyMissing example
class Foo {
    def propertyMissing(String name) {
        "caught missing property: $name"
    }
}

println new Foo().bar
methodMissing example
class MyEmployee {

    def methodMissing(String name, def args){
        println "In missingMethod $name"

        if ( name != 'someMethod'){
            println "Throwing exception"
            throw new MissingMethodException(name, args)
        }
        
        println "Method Missing called on: $name"
        println "with arguments ${args}"
    }
}

MyEmployee me = new MyEmployee()
me.someMethod(1,2,3)                                        // methodMissing is called
me.someOtherMethod(4,5,6)

metaClass

The Groovy metaClass lets you assign behavior and state to Classes at runtime without editing the original source code, it's a layer above the original Class. If you create a class in Groovy and instantiate you have a proerty attached called a metaClass as seen in the image below, so you can access this by using d.metaClass


Groovy has a expando class is a expandable bean under the covers which means we can add fields and methods that don't exist, the metaClass is a expando class so has the same features. Note that all classes have a metaClass this includes for example the String class, this does not mean that you should start adding meta code all over the place, as with all programming use the KISS principle

metaClass example
class Developer {

}

// Expando Class (Meta Class is a Expando Class)
Expando e = new Expando()
e.name = 'Paul'
e.writeCode = { -> println "$name love's to write code...."}
e.writeCode()

Expando e2 = new Expando()
//e2.writeCode()                                                           // code is per instance (not static)

Developer d = new Developer()
d.metaClass.name = "Paul"                                                  // metaClass is a expando class, so we can add properties
d.metaClass.writeCode = { -> println "$name love's to write code...."}
d.writeCode()

// You can even use metaClass for existing Object (this is dangerous)
String.metaClass.shout = { -> toUpperCase() }
println "Hello Paul".shout()

Category Classes

We you may want to add a few additional methods to compiled Java or Groovy classes where we don't have the ability to modify the source code (3rd party), a Groovy category lets us do just that, they allow us to add extra functionality to a new or existing Java or Groovy class.

Category class example
// StringCategory.groovy
class StringCategory {
    static String shout(String str){
        str.toUpperCase()
    }
}


// catDemo.groovy
use(StringCategory) {
    println "Hello,World!".shout()                     // the StringCategory is only available in this block
}

// println "Hello,World!".shout()                      // you cannot use it outside the use block


// time.groovy
use(TimeCategory) {                                    // Groovy has some prebuilt Category classes, TimeCategory for example

    println 1.minute.from.now
    println 10.hours.ago

    def someDate = new Date()
    println someDate - 3.months

}

Intercept-Cache-Invoke Pattern

This is a piece of code to boost performance if you are going to be calling different methods and a way to cache them.

Intercept-Cache-Invoke pattern example
class Developer {

    List languages = []

    def methodMissing(String name, args){

        println "${name}() method was called..."

        if( name.startsWith('write') ) {
            String language = name.split("write")[1]

            if( languages.contains(language) ) {
                def impl = { Object[] theArgs ->
                    println "I like to write code in $language"
                }
                getMetaClass()."$name" = impl                             // points to the above closure
                return impl(args)
            }
        }
    }
}

Developer paul = new Developer()
paul.languages << "Groovy"                         // append to list
paul.languages << "Java"                           // append to list
  println paul.metaClass.methods.size()
  
println paul.metaClass.methods
paul.metaClass.writePerl = { -> println "I like to write code in Perl" }     // looks like I need to do this first, otherwise it crashes
  println paul.metaClass.methods.size()
println paul.metaClass.methods
paul.writeGroovy()
paul.writeGroovy()
paul.writeGroovy()
  println paul.metaClass.methods.size()
paul.writeJava()
paul.writeJava()
  println paul.metaClass.methods.size()
println paul.metaClass.methods