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.
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.
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) |
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() |
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 |