Runtime and compile-time metaprogramming
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. Both have pros and cons that we will detail in this section.
1. Runtime metaprogramming
With runtime metaprogramming we can postpone to runtime the decision to intercept, inject and even synthesize methods of classes and interfaces. For a deep understanding of Groovy’s metaobject protocol (MOP) we need to understand Groovy objects and Groovy’s method handling. In Groovy we work with three kinds of objects: POJO, POGO and Groovy Interceptors. Groovy allows metaprogramming for all types of objects but in a different manner.
-
POJO - A regular Java object whose class can be written in Java or any other language for the JVM.
-
POGO - A Groovy object whose class is written in Groovy. It extends
java.lang.Object
and implements the groovy.lang.GroovyObject interface by default. -
Groovy Interceptor - A Groovy object that implements the groovy.lang.GroovyInterceptable interface and has method-interception capability which is discussed in the GroovyInterceptable section.
For every method call Groovy checks whether the object is a POJO or a POGO. For POJOs, Groovy fetches its MetaClass
from the groovy.lang.MetaClassRegistry and delegates method invocation to it. For POGOs, Groovy takes more steps, as illustrated in the following figure:
1.1. GroovyObject interface
groovy.lang.GroovyObject is the main interface in Groovy as the Object
class is in Java. GroovyObject
has a default implementation in the groovy.lang.GroovyObjectSupport class and it is responsible to transfer invocation to the groovy.lang.MetaClass object. The GroovyObject
source looks like this:
package groovy.lang;
public interface GroovyObject {
Object invokeMethod(String name, Object args);
Object getProperty(String propertyName);
void setProperty(String propertyName, Object newValue);
MetaClass getMetaClass();
void setMetaClass(MetaClass metaClass);
}
1.1.1. invokeMethod
This method is primarily intended to be used in conjunction with the GroovyInterceptable interface or an object’s MetaClass
where it will intercept all method calls.
It is also invoked when the method called is not present on a Groovy object. Here is a simple example using an overridden invokeMethod()
method:
class SomeGroovyClass {
def invokeMethod(String name, Object args) {
return "called invokeMethod $name $args"
}
def test() {
return 'method exists'
}
}
def someGroovyClass = new SomeGroovyClass()
assert someGroovyClass.test() == 'method exists'
assert someGroovyClass.someMethod() == 'called invokeMethod someMethod []'
However, the use of invokeMethod
to intercept missing methods is discouraged. In cases where the intent is to only intercept method calls in the case of a failed method dispatch use methodMissing instead.
1.1.2. get/setProperty
Every read access to a property can be intercepted by overriding the getProperty()
method of the current object. Here is a simple example:
class SomeGroovyClass {
def property1 = 'ha'
def field2 = 'ho'
def field4 = 'hu'
def getField1() {
return 'getHa'
}
def getProperty(String name) {
if (name != 'field3')
return metaClass.getProperty(this, name)
else
return 'field3'
}
}
def someGroovyClass = new SomeGroovyClass()
assert someGroovyClass.field1 == 'getHa'
assert someGroovyClass.field2 == 'ho'
assert someGroovyClass.field3 == 'field3'
assert someGroovyClass.field4 == 'hu'
Forwards the request to the getter for all properties except field3 . |
You can intercept write access to properties by overriding the setProperty()
method:
class POGO {
String property
void setProperty(String name, Object value) {
this.@"$name" = 'overridden'
}
}
def pogo = new POGO()
pogo.property = 'a'
assert pogo.property == 'overridden'
1.1.3. get/setMetaClass
You can access an object’s metaClass
or set your own MetaClass
implementation for changing the default interception mechanism. For example, you can write your own implementation of the MetaClass
interface and assign it to objects in order to change the interception mechanism:
// getMetaclass
someObject.metaClass
// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()
You can find an additional example in the GroovyInterceptable topic. |
1.2. get/setAttribute
This functionality is related to the MetaClass
implementation. In the default implementation you can access fields without invoking their getters and setters. The examples below demonstrates this approach:
class SomeGroovyClass {
def field1 = 'ha'
def field2 = 'ho'
def getField1() {
return 'getHa'
}
}
def someGroovyClass = new SomeGroovyClass()
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field1') == 'ha'
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field2') == 'ho'
class POGO {
private String field
String property1
void setProperty1(String property1) {
this.property1 = "setProperty1"
}
}
def pogo = new POGO()
pogo.metaClass.setAttribute(pogo, 'field', 'ha')
pogo.metaClass.setAttribute(pogo, 'property1', 'ho')
assert pogo.field == 'ha'
assert pogo.property1 == 'ho'
1.3. methodMissing
Groovy supports the concept of methodMissing
. This method differs from invokeMethod
in that it is only invoked in the case of a failed method dispatch when no method can be found for the given name and/or the given arguments:
class Foo {
def methodMissing(String name, def args) {
return "this is me"
}
}
assert new Foo().someUnknownMethod(42l) == 'this is me'
Typically when using methodMissing
it is possible to cache the result for the next time the same method is called.
For example, consider dynamic finders in GORM. These are implemented in terms of methodMissing
. The code resembles something like this:
class GORM {
def dynamicMethods = [...] // an array of dynamic methods that use regex
def methodMissing(String name, args) {
def method = dynamicMethods.find { it.match(name) }
if(method) {
GORM.metaClass."$name" = { Object[] varArgs ->
method.invoke(delegate, name, varArgs)
}
return method.invoke(delegate,name, args)
}
else throw new MissingMethodException(name, delegate, args)
}
}
Notice how, if we find a method to invoke, we then dynamically register a new method on the fly using ExpandoMetaClass. This is so that the next time the same method is called it is more efficient. This way of using methodMissing
does not have the overhead of invokeMethod
and is not expensive from the second call on.
1.4. propertyMissing
Groovy supports the concept of propertyMissing
for intercepting otherwise failing property resolution attempts. In the case of a getter method, propertyMissing
takes a single String
argument containing the property name:
class Foo {
def propertyMissing(String name) { name }
}
assert new Foo().boo == 'boo'
The propertyMissing(String)
method is only called when no getter method for the given property can be found by the Groovy runtime.
For setter methods a second propertyMissing
definition can be added that takes an additional value argument:
class Foo {
def storage = [:]
def propertyMissing(String name, value) { storage[name] = value }
def propertyMissing(String name) { storage[name] }
}
def f = new Foo()
f.foo = "bar"
assert f.foo == "bar"
As with methodMissing
it is best practice to dynamically register new properties at runtime to improve the overall lookup performance.
1.5. static methodMissing
Static variant of methodMissing
method can be added via the ExpandoMetaClass or can be implemented at the class level with $static_methodMissing
method.
class Foo {
static def $static_methodMissing(String name, Object args) {
return "Missing static method name is $name"
}
}
assert Foo.bar() == 'Missing static method name is bar'
1.6. static propertyMissing
Static variant of propertyMissing
method can be added via the ExpandoMetaClass or can be implemented at the class level with $static_propertyMissing
method.
class Foo {
static def $static_propertyMissing(String name) {
return "Missing static property name is $name"
}
}
assert Foo.foobar == 'Missing static property name is foobar'
1.7. GroovyInterceptable
The groovy.lang.GroovyInterceptable interface is marker interface that extends GroovyObject
and is used to notify the Groovy runtime that all methods should be intercepted through the method dispatcher mechanism of the Groovy runtime.
package groovy.lang;
public interface GroovyInterceptable extends GroovyObject {
}
When a Groovy object implements the GroovyInterceptable
interface, its invokeMethod()
is called for any method calls. Below you can see a simple example of an object of this type:
class Interception implements GroovyInterceptable {
def definedMethod() { }
def invokeMethod(String name, Object args) {
'invokedMethod'
}
}
The next piece of code is a test which shows that both calls to existing and non-existing methods will return the same value.
class InterceptableTest extends GroovyTestCase {
void testCheckInterception() {
def interception = new Interception()
assert interception.definedMethod() == 'invokedMethod'
assert interception.someMethod() == 'invokedMethod'
}
}
We cannot use default groovy methods like println because these methods are injected into all Groovy objects so they will be intercepted too. |
If we want to intercept all method calls but do not want to implement the GroovyInterceptable
interface we can implement invokeMethod()
on an object’s MetaClass
. This approach works for both POGOs and POJOs, as shown by this example:
class InterceptionThroughMetaClassTest extends GroovyTestCase {
void testPOJOMetaClassInterception() {
String invoking = 'ha'
invoking.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert invoking.length() == 'invoked'
assert invoking.someMethod() == 'invoked'
}
void testPOGOMetaClassInterception() {
Entity entity = new Entity('Hello')
entity.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert entity.build(new Object()) == 'invoked'
assert entity.someMethod() == 'invoked'
}
}
Additional information about MetaClass can be found in the MetaClasses section. |
1.8. Categories
There are situations where it is useful if a class not under control had additional methods. In order to enable this capability, Groovy implements a feature borrowed from Objective-C, called Categories.
Categories are implemented with so-called category classes. A category class is special in that it needs to meet certain pre-defined rules for defining extension methods.
There are a few categories that are included in the system for adding functionality to classes that make them more usable within the Groovy environment:
Category classes aren’t enabled by default. To use the methods defined in a category class it is necessary to apply the scoped use
method that is provided by the GDK and available from inside every Groovy object instance:
use(TimeCategory) {
println 1.minute.from.now
println 10.hours.ago
def someDate = new Date()
println someDate - 3.months
}
TimeCategory adds methods to Integer |
|
TimeCategory adds methods to Date |
The use
method takes the category class as its first parameter and a closure code block as second parameter. Inside the Closure
access to the category methods is available. As can be seen in the example above even JDK classes like java.lang.Integer
or java.util.Date
can be enriched with user-defined methods.
A category needs not to be directly exposed to the user code, the following will also do:
class JPACategory{
// Let's enhance JPA EntityManager without getting into the JSR committee
static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
entities?.each { em.persist(it) }
}
}
def transactionContext = {
EntityManager em, Closure c ->
def tx = em.transaction
try {
tx.begin()
use(JPACategory) {
c()
}
tx.commit()
} catch (e) {
tx.rollback()
} finally {
//cleanup your resource here
}
}
// user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them.
EntityManager em; //probably injected
transactionContext (em) {
em.persistAll(obj1, obj2, obj3)
// let's do some logics here to make the example sensible
em.persistAll(obj2, obj4, obj6)
}
When we have a look at the groovy.time.TimeCategory
class we see that the extension methods are all declared as static
methods. In fact, this is one of the requirements that must be met by category classes for its methods to be successfully added to a class inside the use
code block:
public class TimeCategory {
public static Date plus(final Date date, final BaseDuration duration) {
return duration.plus(date);
}
public static Date minus(final Date date, final BaseDuration duration) {
final Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.YEAR, -duration.getYears());
cal.add(Calendar.MONTH, -duration.getMonths());
cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
cal.add(Calendar.MINUTE, -duration.getMinutes());
cal.add(Calendar.SECOND, -duration.getSeconds());
cal.add(Calendar.MILLISECOND, -duration.getMillis());
return cal.getTime();
}
// ...
Another requirement is the first argument of the static method must define the type the method is attached to once being activated. The other arguments are the normal arguments the method will take as parameters.
Because of the parameter and static method convention, category method definitions may be a bit less intuitive than normal method definitions. As an alternative Groovy comes with a @Category
annotation that transforms annotated classes into category classes at compile-time.
class Distance {
def number
String toString() { "${number}m" }
}
@Category(Number)
class NumberCategory {
Distance getMeters() {
new Distance(number: this)
}
}
use (NumberCategory) {
assert 42.meters.toString() == '42m'
}
Applying the @Category
annotation has the advantage of being able to use instance methods without the target type as a first parameter. The target type class is given as an argument to the annotation instead.
There is a distinct section on @Category in the compile-time metaprogramming section. |
1.9. Metaclasses
As explained earlier, Metaclasses play a central role in method resolution. For every method invocation from groovy code, Groovy will find the MetaClass
for the given object and delegate the method resolution to the metaclass via MetaClass#invokeMethod which should not be confused with GroovyObject#invokeMethod which happens to be a method that the metaclass may eventually call.
1.9.1. The default metaclass MetaClassImpl
By default, objects get an instance of MetaClassImpl
that implements the default method lookup. This method lookup includes looking up of the method in the object class ("regular" method) but also if no method is found this way it will resort to calling methodMissing
and ultimately GroovyObject#invokeMethod
class Foo {}
def f = new Foo()
assert f.metaClass =~ /MetaClassImpl/
1.9.2. Custom metaclasses
You can change the metaclass of any object or class and replace with a custom implementation of the MetaClass
interface. Usually you will want to subclass one of the existing metaclasses MetaClassImpl
, DelegatingMetaClass
, ExpandoMetaClass
, ProxyMetaClass
, etc. otherwise you will need to implement the complete method lookup logic. Before using a new metaclass instance you should call groovy.lang.MetaClass#initialize() otherwise the metaclass may or may not behave as expected.
Delegating metaclass
If you only need to decorate an existing metaclass the DelegatingMetaClass
simplifies that use case. The old metaclass implementation is still accessible via super
making easy to apply pretransformations to the inputs, routing to other methods and postprocess the outputs.
class Foo { def bar() { "bar" } }
class MyFooMetaClass extends DelegatingMetaClass {
MyFooMetaClass(MetaClass metaClass) { super(metaClass) }
MyFooMetaClass(Class theClass) { super(theClass) }
Object invokeMethod(Object object, String methodName, Object[] args) {
def result = super.invokeMethod(object,methodName.toLowerCase(), args)
result.toUpperCase();
}
}
def mc = new MyFooMetaClass(Foo.metaClass)
mc.initialize()
Foo.metaClass = mc
def f = new Foo()
assert f.BAR() == "BAR" // the new metaclass routes .BAR() to .bar() and uppercases the result
Magic package
It is possible to change the metaclass at startup time by giving the metaclass a specially crafted (magic) class name and package name. In order to change the metaclass for java.lang.Integer
it’s enough to put a class groovy.runtime.metaclass.java.lang.IntegerMetaClass
in the classpath. This is useful, for example, when working with frameworks if you want to do metaclass changes before your code is executed by the framework. The general form of the magic package is groovy.runtime.metaclass.[package].[class]MetaClass
. In the example below the [package]
is java.lang
and the [class]
is Integer
:
// file: IntegerMetaClass.groovy
package groovy.runtime.metaclass.java.lang;
class IntegerMetaClass extends DelegatingMetaClass {
IntegerMetaClass(MetaClass metaClass) { super(metaClass) }
IntegerMetaClass(Class theClass) { super(theClass) }
Object invokeMethod(Object object, String name, Object[] args) {
if (name =~ /isBiggerThan/) {
def other = name.split(/isBiggerThan/)[1].toInteger()
object > other
} else {
return super.invokeMethod(object,name, args);
}
}
}
By compiling the above file with groovyc IntegerMetaClass.groovy
a ./groovy/runtime/metaclass/java/lang/IntegerMetaClass.class
will be generated. The example below will use this new metaclass:
// File testInteger.groovy
def i = 10
assert i.isBiggerThan5()
assert !i.isBiggerThan15()
println i.isBiggerThan5()
By running that file with groovy -cp . testInteger.groovy
the IntegerMetaClass
will be in the classpath and therefore it will become the metaclass for java.lang.Integer
intercepting the method calls to isBiggerThan*()
methods.
1.9.3. Per instance metaclass
You can change the metaclass of individual objects separately, so it’s possible to have multiple object of the same class with different metaclasses.
class Foo { def bar() { "bar" }}
class FooMetaClass extends DelegatingMetaClass {
FooMetaClass(MetaClass metaClass) { super(metaClass) }
Object invokeMethod(Object object, String name, Object[] args) {
super.invokeMethod(object,name,args).toUpperCase()
}
}
def f1 = new Foo()
def f2 = new Foo()
f2.metaClass = new FooMetaClass(f2.metaClass)
assert f1.bar() == "bar"
assert f2.bar() == "BAR"
assert f1.metaClass =~ /MetaClassImpl/
assert f2.metaClass =~ /FooMetaClass/
assert f1.class.toString() == "class Foo"
assert f2.class.toString() == "class Foo"
1.9.4. ExpandoMetaClass
Groovy comes with a special MetaClass
the so-called ExpandoMetaClass
. It is special in that it allows for dynamically adding or changing methods, constructors, properties and even static methods by using a neat closure syntax.
Applying those modifications can be especially useful in mocking or stubbing scenarios as shown in the Testing Guide.
Every java.lang.Class
is supplied by Groovy with a special metaClass
property that will give you a reference to an ExpandoMetaClass
instance. This instance can then be used to add methods or change the behaviour of already existing ones.
By default ExpandoMetaClass doesn’t do inheritance. To enable this you must call ExpandoMetaClass#enableGlobally() before your app starts such as in the main method or servlet bootstrap. |
The following sections go into detail on how ExpandoMetaClass
can be used in various scenarios.
Methods
Once the ExpandoMetaClass
is accessed by calling the metaClass
property, methods can added by using either the left shift <<
or the =
operator.
Note that the left shift operator is used to append a new method. If a public method with the same name and parameter types is declared by the class or interface, including those inherited from superclasses and superinterfaces but excluding those added to the metaClass at runtime, an exception will be thrown. If you want to replace a method declared by the class or interface you can use the = operator. |
The operators are applied on a non-existent property of metaClass
passing an instance of a Closure
code block.
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
The example above shows how a new method can be added to a class by accessing the metaClass
property and using the <<
or =
operator to assign a Closure
code block. The Closure
parameters are interpreted as method parameters. Parameterless methods can be added by using the {→ …}
syntax.
Properties
ExpandoMetaClass
supports two mechanisms for adding or overriding properties.
Firstly, it has support for declaring a mutable property by simply assigning a value to a property of metaClass
:
class Book {
String title
}
Book.metaClass.author = "Stephen King"
def b = new Book()
assert "Stephen King" == b.author
Another way is to add getter and/or setter methods by using the standard mechanisms for adding instance methods.
class Book {
String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }
def b = new Book()
assert "Stephen King" == b.author
In the source code example above the property is dictated by the closure and is a read-only property. It is feasible to add an equivalent setter method but then the property value needs to be stored for later usage. This could be done as shown in the following example.
class Book {
String title
}
def properties = Collections.synchronizedMap([:])
Book.metaClass.setAuthor = { String value ->
properties[System.identityHashCode(delegate) + "author"] = value
}
Book.metaClass.getAuthor = {->
properties[System.identityHashCode(delegate) + "author"]
}
This is not the only technique however. For example in a servlet container one way might be to store the values in the currently executing request as request attributes (as is done in some cases in Grails).
Constructors
Constructors can be added by using a special constructor
property. Either the <<
or =
operator can be used to assign a Closure
code block. The Closure
arguments will become the constructor arguments when the code is executed at runtime.
class Book {
String title
}
Book.metaClass.constructor << { String title -> new Book(title:title) }
def book = new Book('Groovy in Action - 2nd Edition')
assert book.title == 'Groovy in Action - 2nd Edition'
Be careful when adding constructors however, as it is very easy to get into stack overflow troubles. |
Static Methods
Static methods can be added using the same technique as instance methods with the addition of the static
qualifier before the method name.
class Book {
String title
}
Book.metaClass.static.create << { String title -> new Book(title:title) }
def b = Book.create("The Stand")
Borrowing Methods
With ExpandoMetaClass
it is possible to use Groovy’s method pointer syntax to borrow methods from other classes.
class Person {
String name
}
class MortgageLender {
def borrowMoney() {
"buy house"
}
}
def lender = new MortgageLender()
Person.metaClass.buyHouse = lender.&borrowMoney
def p = new Person()
assert "buy house" == p.buyHouse()
Dynamic Method Names
Since Groovy allows you to use Strings as property names this in turns allows you to dynamically create method and property names at runtime. To create a method with a dynamic name simply use the language feature of reference property names as strings.
class Person {
String name = "Fred"
}
def methodName = "Bob"
Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }
def p = new Person()
assert "Fred" == p.name
p.changeNameToBob()
assert "Bob" == p.name
The same concept can be applied to static methods and properties.
One application of dynamic method names can be found in the Grails web application framework. The concept of "dynamic codecs" is implemented by using dynamic method names.
HTMLCodec
Classclass HTMLCodec {
static encode = { theTarget ->
HtmlUtils.htmlEscape(theTarget.toString())
}
static decode = { theTarget ->
HtmlUtils.htmlUnescape(theTarget.toString())
}
}
The example above shows a codec implementation. Grails comes with various codec implementations each defined in a single class. At runtime there will be multiple codec classes in the application classpath. At application startup the framework adds a encodeXXX
and a decodeXXX
method to certain meta-classes where XXX
is the first part of the codec class name (e.g. encodeHTML
). This mechanism is in the following shown in some Groovy pseudo-code:
def codecs = classes.findAll { it.name.endsWith('Codec') }
codecs.each { codec ->
Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
}
def html = '<html><body>hello</body></html>'
assert '<html><body>hello</body></html>' == html.encodeAsHTML()
Runtime Discovery
At runtime it is often useful to know what other methods or properties exist at the time the method is executed. ExpandoMetaClass
provides the following methods as of this writing:
-
getMetaMethod
-
hasMetaMethod
-
getMetaProperty
-
hasMetaProperty
Why can’t you just use reflection? Well because Groovy is different, it has the methods that are "real" methods and methods that are available only at runtime. These are sometimes (but not always) represented as MetaMethods. The MetaMethods tell you what methods are available at runtime, thus your code can adapt.
This is of particular use when overriding invokeMethod
, getProperty
and/or setProperty
.
GroovyObject Methods
Another feature of ExpandoMetaClass
is that it allows to override the methods invokeMethod
, getProperty
and setProperty
, all of them can be found in the groovy.lang.GroovyObject
class.
The following example shows how to override invokeMethod
:
class Stuff {
def invokeMe() { "foo" }
}
Stuff.metaClass.invokeMethod = { String name, args ->
def metaMethod = Stuff.metaClass.getMetaMethod(name, args)
def result
if(metaMethod) result = metaMethod.invoke(delegate,args)
else {
result = "bar"
}
result
}
def stf = new Stuff()
assert "foo" == stf.invokeMe()
assert "bar" == stf.doStuff()
The first step in the Closure
code is to lookup the MetaMethod
for the given name and arguments. If the method can be found everything is fine and it is delegated to. If not, a dummy value is returned.
A MetaMethod is a method that is known to exist on the MetaClass whether added at runtime or at compile-time. |
The same logic can be used to override setProperty
or getProperty
.
class Person {
String name = "Fred"
}
Person.metaClass.getProperty = { String name ->
def metaProperty = Person.metaClass.getMetaProperty(name)
def result
if(metaProperty) result = metaProperty.getProperty(delegate)
else {
result = "Flintstone"
}
result
}
def p = new Person()
assert "Fred" == p.name
assert "Flintstone" == p.other
The important thing to note here is that instead of a MetaMethod
a MetaProperty
instance is looked up. If that exists the getProperty
method of the MetaProperty
is called, passing the delegate.
Overriding Static invokeMethod
ExpandoMetaClass
even allows for overriding static method with a special invokeMethod
syntax.
class Stuff {
static invokeMe() { "foo" }
}
Stuff.metaClass.'static'.invokeMethod = { String name, args ->
def metaMethod = Stuff.metaClass.getStaticMetaMethod(name, args)
def result
if(metaMethod) result = metaMethod.invoke(delegate,args)
else {
result = "bar"
}
result
}
assert "foo" == Stuff.invokeMe()
assert "bar" == Stuff.doStuff()
The logic that is used for overriding the static method is the same as we’ve seen before for overriding instance methods. The only difference is the access to the metaClass.static
property and the call to getStaticMethodName
for retrieving the static MetaMethod
instance.
Extending Interfaces
It is possible to add methods onto interfaces with ExpandoMetaClass
. To do this however, it must be enabled globally using the ExpandoMetaClass.enableGlobally()
method before application start-up.
List.metaClass.sizeDoubled = {-> delegate.size() * 2 }
def list = []
list << 1
list << 2
assert 4 == list.sizeDoubled()
1.10. Extension modules
1.10.1. Extending existing classes
An extension module allows you to add new methods to existing classes, including classes which are precompiled, like classes from the JDK. Those new methods, unlike those defined through a metaclass or using a category, are available globally. For example, when you write:
def file = new File(...)
def contents = file.getText('utf-8')
The getText
method doesn’t exist on the File
class. However, Groovy knows it because it is defined in a special class, ResourceGroovyMethods
:
public static String getText(File file, String charset) throws IOException {
return IOGroovyMethods.getText(newReader(file, charset));
}
You may notice that the extension method is defined using a static method in a helper class (where various extension methods are defined). The first argument of the getText
method corresponds to the receiver, while additional parameters correspond to the arguments of the extension method. So here, we are defining a method called getText on the File
class (because the first argument is of type File
), which takes a single argument as a parameter (the encoding String
).
The process of creating an extension module is simple:
-
write an extension class like above
-
write a module descriptor file
Then you have to make the extension module visible to Groovy, which is as simple as having the extension module classes and descriptor available on classpath. This means that you have the choice:
-
either provide the classes and module descriptor directly on classpath
-
or bundle your extension module into a jar for reusability
An extension module may add two kind of methods to a class:
-
instance methods (to be called on an instance of a class)
-
static methods (to be called on the class itself)
1.10.2. Instance methods
To add an instance method to an existing class, you need to create an extension class. For example, let’s say you want to add a maxRetries
method on Integer
which accepts a closure and executes it at most n times until no exception is thrown. To do that, you only need to write the following:
class MaxRetriesExtension {
static void maxRetries(Integer self, Closure code) {
assert self >= 0
int retries = self
Throwable e = null
while (retries > 0) {
try {
code.call()
break
} catch (Throwable err) {
e = err
retries--
}
}
if (retries == 0 && e) {
throw e
}
}
}
The extension class | |
First argument of the static method corresponds to the receiver of the message, that is to say the extended instance |
Then, after having declared your extension class, you can call it this way:
int i=0
5.maxRetries {
i++
}
assert i == 1
i=0
try {
5.maxRetries {
i++
throw new RuntimeException("oops")
}
} catch (RuntimeException e) {
assert i == 5
}
1.10.3. Static methods
It is also possible to add static methods to a class. In that case, the static method needs to be defined in its own file. Static and instance extension methods cannot be present in the same class.
class StaticStringExtension {
static String greeting(String self) {
'Hello, world!'
}
}
The static extension class | |
First argument of the static method corresponds to the class being extended and is unused |
In which case you can call it directly on the String
class:
assert String.greeting() == 'Hello, world!'
1.10.4. Module descriptor
For Groovy to be able to load your extension methods, you must declare your extension helper classes. You must create a file named org.codehaus.groovy.runtime.ExtensionModule
into the META-INF/groovy
directory:
moduleName=Test module for specifications moduleVersion=1.0-test extensionClasses=support.MaxRetriesExtension staticExtensionClasses=support.StaticStringExtension
The module descriptor requires 4 keys:
-
moduleName : the name of your module
-
moduleVersion: the version of your module. Note that version number is only used to check that you don’t load the same module in two different versions.
-
extensionClasses: the list of extension helper classes for instance methods. You can provide several classes, given that they are comma separated.
-
staticExtensionClasses: the list of extension helper classes for static methods. You can provide several classes, given that they are comma separated.
Note that it is not required for a module to define both static helpers and instance helpers, and that you may add several classes to a single module. You can also extend different classes in a single module without problem. It is even possible to use different classes in a single extension class, but it is recommended to group extension methods into classes by feature set.
1.10.5. Extension modules and classpath
It’s worth noting that you can’t use an extension which is compiled at the same time as code using it. That means that to use an extension, it has to be available on classpath, as compiled classes, before the code using it gets compiled. Usually, this means that you can’t have the test classes in the same source unit as the extension class itself. Since in general, test sources are separated from normal sources and executed in another step of the build, this is not an issue.
1.10.6. Compatibility with type checking
Unlike categories, extension modules are compatible with type checking: if they are found on classpath, then the type checker is aware of the extension methods and will not complain when you call them. It is also compatible with static compilation.
2. Compile-time metaprogramming
Compile-time metaprogramming in Groovy allows code generation at compile-time. Those transformations are altering the Abstract Syntax Tree (AST) of a program, which is why in Groovy we call it AST transformations. AST transformations allow you to hook into the compilation process, modify the AST and continue the compilation process to generate regular bytecode. Compared to runtime metaprogramming, this has the advantage of making the changes visible in the class file itself (that is to say, in the bytecode). Making it visible in the bytecode is important for example if you want the transformations to be part of the class contract (implementing interfaces, extending abstract classes, …) or even if you need your class to be callable from Java (or other JVM languages). For example, an AST transformation can add methods to a class. If you do it with runtime metaprogramming, the new method would only be visible from Groovy. If you do the same using compile-time metaprogramming, the method would be visible from Java too. Last but not least, performance would likely be better with compile-time metaprogramming (because no initialization phase is required).
In this section, we will start with explaining the various compile-time transformations that are bundled with the Groovy distribution. In a subsequent section, we will describe how you can implement your own AST transformations and what are the disadvantages of this technique.
2.1. Available AST transformations
Groovy comes with various AST transformations covering different needs: reducing boilerplate (code generation), implementing design patterns (delegation, …), logging, declarative concurrency, cloning, safer scripting, tweaking the compilation, implementing Swing patterns, testing and eventually managing dependencies. If none of those AST transformations cover your needs, you can still implement your own, as show in section Developing your own AST transformations.
AST transformations can be separated into two categories:
-
global AST transformations are applied transparently, globally, as soon as they are found on compile classpath
-
local AST transformations are applied by annotating the source code with markers. Unlike global AST transformations, local AST transformations may support parameters.
Groovy doesn’t ship with any global AST transformation, but you can find a list of local AST transformations available for you to use in your code here:
2.1.1. Code generation transformations
This category of transformation includes AST transformations which help removing boilerplate code. This is typically code that you have to write but that does not carry any useful information. By autogenerating this boilerplate code, the code you have to write is left clean and concise and the chance of introducing an error by getting such boilerplate code incorrect is reduced.
@groovy.transform.ToString
The @ToString
AST transformation generates a human readable toString
representation of the class. For example, annotating the Person
class like below will automatically generate the toString
method for you:
import groovy.transform.ToString
@ToString
class Person {
String firstName
String lastName
}
With this definition, then the following assertion passes, meaning that a toString
method taking the field values from the class and printing them out has been generated:
def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Jack, Nicholson)'
The @ToString
annotation accepts several parameters which are summarized in the following table:
Attribute | Default value | Description | Example |
---|---|---|---|
excludes |
Empty list |
List of properties to exclude from toString |
|
includes |
Undefined marker list (indicates all fields) |
List of fields to include in toString |
|
includeSuper |
False |
Should superclass be included in toString |
|
includeNames |
false |
Whether to include names of properties in generated toString. |
|
includeFields |
False |
Should fields be included in toString, in addition to properties |
|
includeSuperProperties |
False |
Should super properties be included in toString |
|
includeSuperFields |
False |
Should visible super fields be included in toString |
|
ignoreNulls |
False |
Should properties/fields with null value be displayed |
|
includePackage |
True |
Use fully qualified class name instead of simple name in toString |
|
allProperties |
True |
Include all JavaBean properties in toString |
|
cache |
False |
Cache the toString string. Should only be set to true if the class is immutable. |
|
allNames |
False |
Should fields and/or properties with internal names be included in the generated toString |
|
@groovy.transform.EqualsAndHashCode
The @EqualsAndHashCode
AST transformation aims at generating equals
and hashCode
methods for you. The generated hashcode follows the best practices as described in Effective Java by Josh Bloch:
import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1==p2
assert p1.hashCode() == p2.hashCode()
There are several options available to tweak the behavior of @EqualsAndHashCode
:
Attribute | Default value | Description | Example |
---|---|---|---|
excludes |
Empty list |
List of properties to exclude from equals/hashCode |
|
includes |
Undefined marker list (indicating all fields) |
List of fields to include in equals/hashCode |
|
cache |
False |
Cache the hashCode computation. Should only be set to true if the class is immutable. |
|
callSuper |
False |
Whether to include super in equals and hashCode calculations |
|
includeFields |
False |
Should fields be included in equals/hashCode, in addition to properties |
|
useCanEqual |
True |
Should equals call canEqual helper method. |
|
allProperties |
False |
Should JavaBean properties be included in equals and hashCode calculations |
|
allNames |
False |
Should fields and/or properties with internal names be included in equals and hashCode calculations |
|
@groovy.transform.TupleConstructor
The @TupleConstructor
annotation aims at eliminating boilerplate code by generating constructors for you. A tuple constructor is created having a parameter for each property (and possibly each field). Each parameter has a default value (using the initial value of the property if present or otherwise Java’s default value according to the properties type).
Implementation Details
Normally you don’t need to understand the imp[ementation details of the generated constructor(s); you just use them in the normal way. However, if you want to add multiple constructors, understand Java integration options or meet requirements of some dependency injection frameworks, then some details are useful.
As previously mentioned, the generated constructor has default values applied. In later compilation phases, the Groovy compiler’s standard default value processing behavior is then applied. The end result is that multiple constructors are placed within the bytecode of your class. This provides a well understood semantics and is also useful for Java integration purposes. As an example, the following code will generate 3 constructors:
import groovy.transform.TupleConstructor
@TupleConstructor
class Person {
String firstName
String lastName
}
// traditional map-style constructor
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
// generated tuple constructor
def p2 = new Person('Jack', 'Nicholson')
// generated tuple constructor with default value for second property
def p3 = new Person('Jack')
The first constructor is a no-arg constructor which allows the traditional map-style construction so long as you don’t have final properties. Groovy calls the no-arg constructor and then the relevant setters under the covers. It is worth noting that if the first property (or field) has type LinkedHashMap or if there is a single Map, AbstractMap or HashMap property (or field), then the map-style named arguments won’t be available.
The other constructors are generated by taking the properties in the order they are defined. Groovy will generate as many constructors as there are properties (or fields, depending on the options).
Setting the defaults
attribute (see the available configuration options table) to false
, disables the normal default values behavior which means:
-
Exactly one constructor will be produced
-
Attempting to use an initial value will produce an error
-
Map-style named arguments won’t be available
This attribute is normally only used in situations where another Java framework is expecting exactly one constructor, e.g. injection frameworks or JUnit parameterized runners.
Immutability support
If the @PropertyOptions
annotation is also found on the class with the @TupleConstructor
annotation, then the generated constructor may contain custom property handling logic. The propertyHandler
attribute on the @PropertyOptions
annotation could for instance be set to ImmutablePropertyHandler
which will result in the addition of the necessary logic for immutable classes (defensive copy in, cloning, etc.). This normally would happen automatically behind the scenes when you use the @Immutable
meta-annotation. Some of the annotation attributes might not be supported by all property handlers.
Customization options
The @TupleConstructor
AST transformation accepts several annotation attributes:
Attribute | Default value | Description | Example |
---|---|---|---|
excludes |
Empty list |
List of properties to exclude from tuple constructor generation |
|
includes |
Undefined list (indicates all fields) |
List of fields to include in tuple constructor generation |
|
includeProperties |
True |
Should properties be included in tuple constructor generation |
|
includeFields |
False |
Should fields be included in tuple constructor generation, in addition to properties |
|
includeSuperProperties |
True |
Should properties from super classes be included in tuple constructor generation |
|
includeSuperFields |
False |
Should fields from super classes be included in tuple constructor generation |
|
callSuper |
False |
Should super properties be called within a call to the parent constructor rather than set as properties |
|
force |
False |
By default, the transformation will do nothing if a constructor is already defined. Setting this attribute to true, the constructor will be generated and it’s your responsibility to ensure that no duplicate constructor is defined. |
|
defaults |
True |
Indicates that default value processing is enabled for constructor parameters. Set to false to obtain exactly one constructor but with initial value support and named-arguments disabled. |
|
useSetters |
False |
By default, the transformation will directly set the backing field of each property from its corresponding constructor parameter. Setting this attribute to true, the constructor will instead call setters if they exist. It’s usually deemed bad style from within a constructor to call setters that can be overridden. It’s your responsibility to avoid such bad style. |
|
allNames |
False |
Should fields and/or properties with internal names be included within the constructor |
|
allProperties |
False |
Should JavaBean properties be included within the constructor |
|
pre |
empty |
A closure containing statements to be inserted at the start of the generated constructor(s) |
|
post |
empty |
A closure containing statements to be inserted at the end of the generated constructor(s) |
|
Setting the defaults
annotation attribute to false
and the force
annotation attribute to true
allows multiple tuple constructors to be created by using different customization options for the different cases (provided each case has a different type signature) as shown in the following example:
class Named {
String name
}
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false)
@TupleConstructor(force=true, defaults=false, includeFields=true)
@TupleConstructor(force=true, defaults=false, includeSuperProperties=true)
class Book extends Named {
Integer published
private Boolean fiction
Book() {}
}
assert new Book("Regina", 2015).toString() == 'Book(published:2015, name:Regina)'
assert new Book(2015, false).toString() == 'Book(published:2015, fiction:false)'
assert new Book(2015).toString() == 'Book(published:2015)'
assert new Book().toString() == 'Book()'
assert Book.constructors.size() == 4
Similarly, here is another example using different options for includes
:
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false, includes='name,year')
@TupleConstructor(force=true, defaults=false, includes='year,fiction')
@TupleConstructor(force=true, defaults=false, includes='name,fiction')
class Book {
String name
Integer year
Boolean fiction
}
assert new Book("Regina", 2015).toString() == 'Book(name:Regina, year:2015)'
assert new Book(2015, false).toString() == 'Book(year:2015, fiction:false)'
assert new Book("Regina", false).toString() == 'Book(name:Regina, fiction:false)'
assert Book.constructors.size() == 3
@groovy.transform.MapConstructor
The @MapConstructor
annotation aims at eliminating boilerplate code by generating a map constructor for you. A map constructor is created such that each property in the class is set based on the value in the supplied map having the key with the name of the property. Usage is as shown in this example:
import groovy.transform.*
@ToString
@MapConstructor
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)'
The generated constructor will be roughly like this:
public Person(Map args) {
if (args.containsKey('firstName')) {
this.firstName = args.get('firstName')
}
if (args.containsKey('lastName')) {
this.lastName = args.get('lastName')
}
}
@groovy.transform.Canonical
The @Canonical
meta-annotation combines the @ToString, @EqualsAndHashCode and @TupleConstructor annotations:
import groovy.transform.Canonical
@Canonical
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)' // Effect of @ToString
def p2 = new Person('Jack','Nicholson') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack, Nicholson)'
assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode
A similar immutable class can be generated using the @Immutable meta-annotation instead. The @Canonical
meta-annotation supports the configuration options found in the annotations it aggregates. See those annotations for more details.
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
The @Canonical
meta-annotation can be used in conjunction with an explicit use one or more of its component annotations, like this:
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
Any applicable annotation attributes from @Canonical
are passed along to the explicit annotation but attributes already existing in the explicit annotation take precedence.
@groovy.transform.InheritConstructors
The @InheritConstructor
AST transformation aims at generating constructors matching super constructors for you. This is in particular useful when overriding exception classes:
import groovy.transform.InheritConstructors
@InheritConstructors
class CustomException extends Exception {}
// all those are generated constructors
new CustomException()
new CustomException("A custom message")
new CustomException("A custom message", new RuntimeException())
new CustomException(new RuntimeException())
// Java 7 only
// new CustomException("A custom message", new RuntimeException(), false, true)
The @InheritConstructor
AST transformation supports the following configuration options:
Attribute | Default value | Description | Example |
---|---|---|---|
constructorAnnotations |
False |
Whether to carry over annotations from the constructor during copying |
|
parameterAnnotations |
False |
Whether to carry over annotations from the constructor parameters when copying the constructor |
|
@groovy.lang.Category
The @Category
AST transformation simplifies the creation of Groovy categories. Historically, a Groovy category was written like this:
class TripleCategory {
public static Integer triple(Integer self) {
3*self
}
}
use (TripleCategory) {
assert 9 == 3.triple()
}
The @Category
transformation lets you write the same using an instance-style class, rather than a static class style. This removes the need for having the first argument of each method being the receiver. The category can be written like this:
@Category(Integer)
class TripleCategory {
public Integer triple() { 3*this }
}
use (TripleCategory) {
assert 9 == 3.triple()
}
Note that the mixed in class can be referenced using this
instead. It’s also worth noting that using instance fields in a category class is inherently unsafe: categories are not stateful (like traits).
@groovy.transform.IndexedProperty
The @IndexedProperty
annotation aims at generating indexed getters/setters for properties of list/array types. This is in particular useful if you want to use a Groovy class from Java. While Groovy supports GPath to access properties, this is not available from Java. The @IndexedProperty
annotation will generate indexed properties of the following form:
class SomeBean {
@IndexedProperty String[] someArray = new String[2]
@IndexedProperty List someList = []
}
def bean = new SomeBean()
bean.setSomeArray(0, 'value')
bean.setSomeList(0, 123)
assert bean.someArray[0] == 'value'
assert bean.someList == [123]
@groovy.lang.Lazy
The @Lazy
AST transformation implements lazy initialization of fields. For example, the following code:
class SomeBean {
@Lazy LinkedList myField
}
will produce the following code:
List $myField
List getMyField() {
if ($myField!=null) { return $myField }
else {
$myField = new LinkedList()
return $myField
}
}
The default value which is used to initialize the field is the default constructor of the declaration type. It is possible to define a default value by using a closure on the right hand side of the property assignment, as in the following example:
class SomeBean { @Lazy LinkedList myField = { ['a','b','c']}() }
In that case, the generated code looks like the following:
List $myField List getMyField() { if ($myField!=null) { return $myField } else { $myField = { ['a','b','c']}() return $myField } }
If the field is declared volatile then initialization will be synchronized using the double-checked locking pattern.
Using the soft=true
parameter, the helper field will use a SoftReference
instead, providing a simple way to implement caching. In that case, if the garbage collector decides to collect the reference, initialization will occur the next time the field is accessed.
@groovy.lang.Newify
The @Newify
AST transformation is used to bring alternative syntaxes to construct objects:
-
Using the
Python
style:
@Newify([Tree,Leaf]) class TreeBuilder { Tree tree = Tree(Leaf('A'),Leaf('B'),Tree(Leaf('C'))) }
-
or using the
Ruby
style:
@Newify([Tree,Leaf]) class TreeBuilder { Tree tree = Tree.new(Leaf.new('A'),Leaf.new('B'),Tree.new(Leaf.new('C'))) }
The Ruby
version can be disabled by setting the auto
flag to false
.
@groovy.transform.Sortable
The @Sortable
AST transformation is used to help write classes that are Comparable
and easily sorted typically by numerous properties. It is easy to use as shown in the following example where we annotate the Person
class:
import groovy.transform.Sortable
@Sortable class Person {
String first
String last
Integer born
}
The generated class has the following properties:
-
it implements the
Comparable
interface -
it contains a
compareTo
method with an implementation based on the natural ordering of thefirst
,last
andborn
properties -
it has three methods returning comparators:
comparatorByFirst
,comparatorByLast
andcomparatorByBorn
.
The generated compareTo
method will look like this:
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.last <=> obj.last
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
As an example of the generated comparators, the comparatorByFirst
comparator will have a compare
method that looks like this:
public int compare(java.lang.Object arg0, java.lang.Object arg1) {
if (arg0 == arg1) {
return 0
}
if (arg0 != null && arg1 == null) {
return -1
}
if (arg0 == null && arg1 != null) {
return 1
}
return arg0.first <=> arg1.first
}
The Person
class can be used wherever a Comparable
is expected and the generated comparators wherever a Comparator
is expected as shown by these examples:
def people = [
new Person(first: 'Johnny', last: 'Depp', born: 1963),
new Person(first: 'Keira', last: 'Knightley', born: 1985),
new Person(first: 'Geoffrey', last: 'Rush', born: 1951),
new Person(first: 'Orlando', last: 'Bloom', born: 1977)
]
assert people[0] > people[2]
assert people.sort()*.last == ['Rush', 'Depp', 'Knightley', 'Bloom']
assert people.sort(false, Person.comparatorByFirst())*.first == ['Geoffrey', 'Johnny', 'Keira', 'Orlando']
assert people.sort(false, Person.comparatorByLast())*.last == ['Bloom', 'Depp', 'Knightley', 'Rush']
assert people.sort(false, Person.comparatorByBorn())*.last == ['Rush', 'Depp', 'Bloom', 'Knightley']
Normally, all properties are used in the generated compareTo
method in the priority order in which they are defined. You can include or exclude certain properties from the generated compareTo
method by giving a list of property names in the includes
or excludes
annotation attributes. If using includes
, the order of the property names given will determine the priority of properties when comparing. To illustrate, consider the following Person
class definition:
@Sortable(includes='first,born') class Person {
String last
int born
String first
}
It will have two comparator methods comparatorByFirst
and comparatorByBorn
and the generated compareTo
method will look like this:
public int compareTo(java.lang.Object obj) {
if (this.is(obj)) {
return 0
}
if (!(obj instanceof Person)) {
return -1
}
java.lang.Integer value = this.first <=> obj.first
if (value != 0) {
return value
}
value = this.born <=> obj.born
if (value != 0) {
return value
}
return 0
}
This Person
class can be used as follows:
def people = [
new Person(first: 'Ben', last: 'Affleck', born: 1972),
new Person(first: 'Ben', last: 'Stiller', born: 1965)
]
assert people.sort()*.last == ['Stiller', 'Affleck']
The behavior of the @Sortable
AST transformation can be further changed using the following additional parameters:
Attribute | Default value | Description | Example |
---|---|---|---|
allProperties |
True |
Should JavaBean properties (ordered after native properties) be used |
|
allNames |
False |
Should properties with "internal" names be used |
|
includeSuperProperties |
False |
Should super properties also be used (ordered first) |
|
@groovy.transform.builder.Builder
The @Builder
AST transformation is used to help write classes that can be created using fluent api calls. The transform supports multiple building strategies to cover a range of cases and there are a number of configuration options to customize the building process. If you’re an AST hacker, you can also define your own strategy class. The following table lists the available strategies that are bundled with Groovy and the configuration options each strategy supports.
Strategy |
Description |
builderClassName |
builderMethodName |
buildMethodName |
prefix |
includes/excludes |
includeSuperProperties |
allNames |
|
chained setters |
n/a |
n/a |
n/a |
yes, default "set" |
yes |
n/a |
yes, default |
|
explicit builder class, class being built untouched |
n/a |
n/a |
yes, default "build" |
yes, default "" |
yes |
yes, default |
yes, default |
|
creates a nested helper class |
yes, default <TypeName>Builder |
yes, default "builder" |
yes, default "build" |
yes, default "" |
yes |
yes, default |
yes, default |
|
creates a nested helper class providing type-safe fluent creation |
yes, default <TypeName>Initializer |
yes, default "createInitializer" |
yes, default "create" but usually only used internally |
yes, default "" |
yes |
yes, default |
yes, default |
To use the SimpleStrategy
, annotate your Groovy class using the @Builder
annotation, and specify the strategy as shown in this example:
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy)
class Person {
String first
String last
Integer born
}
Then, just call the setters in a chained fashion as shown here:
def p1 = new Person().setFirst('Johnny').setLast('Depp').setBorn(1963)
assert "$p1.first $p1.last" == 'Johnny Depp'
For each property, a generated setter will be created which looks like this:
public Person setFirst(java.lang.String first) {
this.first = first
return this
}
You can specify a prefix as shown in this example:
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy, prefix="")
class Person {
String first
String last
Integer born
}
And calling the chained setters would look like this:
def p = new Person().first('Johnny').last('Depp').born(1963)
assert "$p.first $p.last" == 'Johnny Depp'
You can use the SimpleStrategy
in conjunction with @TupleConstructor
. If your @Builder
annotation doesn’t have explicit includes
or excludes
annotation attributes but your @TupleConstructor
annotation does, the ones from @TupleConstructor
will be re-used for @Builder
. The same applies for any annotation aliases which combine @TupleConstructor
such as @Canonical
.
The annotation attribute useSetters
can be used if you have a setter which you want called as part of the construction process. See the JavaDoc for details.
The annotation attributes builderClassName
, buildMethodName
, builderMethodName
, forClass
and includeSuperProperties
are not supported for this strategy.
Groovy already has built-in building mechanisms. Don’t rush to using @Builder if the built-in mechanisms meet your needs. Some examples: |
def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
first = 'Geoffrey'
last = 'Rush'
born = 1951
}
To use the ExternalStrategy
, create and annotate a Groovy builder class using the @Builder
annotation, specify the class the builder is for using forClass
and indicate use of the ExternalStrategy
. Suppose you have the following class you would like a builder for:
class Person {
String first
String last
int born
}
you explicitly create and use your builder class as follows:
import groovy.transform.builder.*
@Builder(builderStrategy=ExternalStrategy, forClass=Person)
class PersonBuilder { }
def p = new PersonBuilder().first('Johnny').last('Depp').born(1963).build()
assert "$p.first $p.last" == 'Johnny Depp'
Note that the (normally empty) builder class you provide will be filled in with appropriate setters and a build method. The generated build method will look something like:
public Person build() {
Person _thePerson = new Person()
_thePerson.first = first
_thePerson.last = last
_thePerson.born = born
return _thePerson
}
The class you are creating the builder for can be any Java or Groovy class following the normal JavaBean conventions, e.g. a no-arg constructor and setters for the properties. Here is an example using a Java class:
import groovy.transform.builder.*
@Builder(builderStrategy=ExternalStrategy, forClass=javax.swing.DefaultButtonModel)
class ButtonModelBuilder {}
def model = new ButtonModelBuilder().enabled(true).pressed(true).armed(true).rollover(true).selected(true).build()
assert model.isArmed()
assert model.isPressed()
assert model.isEnabled()
assert model.isSelected()
assert model.isRollover()
The generated builder can be customised using the prefix
, includes
, excludes
and buildMethodName
annotation attributes. Here is an example illustrating various customisations:
import groovy.transform.builder.*
import groovy.transform.Canonical
@Canonical
class Person {
String first
String last
int born
}
@Builder(builderStrategy=ExternalStrategy, forClass=Person, includes=['first', 'last'], buildMethodName='create', prefix='with')
class PersonBuilder { }
def p = new PersonBuilder().withFirst('Johnny').withLast('Depp').create()
assert "$p.first $p.last" == 'Johnny Depp'
The builderMethodName
and builderClassName
annotation attributes for @Builder
aren’t applicable for this strategy.
You can use the ExternalStrategy
in conjunction with @TupleConstructor
. If your @Builder
annotation doesn’t have explicit includes
or excludes
annotation attributes but the @TupleConstructor
annotation of the class you are creating the builder for does, the ones from @TupleConstructor
will be re-used for @Builder
. The same applies for any annotation aliases which combine @TupleConstructor
such as @Canonical
.
To use the DefaultStrategy
, annotate your Groovy class using the @Builder
annotation as shown in this example:
import groovy.transform.builder.Builder
@Builder
class Person {
String firstName
String lastName
int age
}
def person = Person.builder().firstName("Robert").lastName("Lewandowski").age(21).build()
assert person.firstName == "Robert"
assert person.lastName == "Lewandowski"
assert person.age == 21
If you want, you can customize various aspects of the building process using the builderClassName
, buildMethodName
, builderMethodName
, prefix
, includes
and excludes
annotation attributes, some of which are used in the example here:
import groovy.transform.builder.Builder
@Builder(buildMethodName='make', builderMethodName='maker', prefix='with', excludes='age')
class Person {
String firstName
String lastName
int age
}
def p = Person.maker().withFirstName("Robert").withLastName("Lewandowski").make()
assert "$p.firstName $p.lastName" == "Robert Lewandowski"
This strategy also supports annotating static methods and constructors. In this case, the static method or constructor parameters become the properties to use for building purposes and in the case of static methods, the return type of the method becomes the target class being built. If you have more than one @Builder
annotation used within a class (at either the class, method or constructor positions) then it is up to you to ensure that the generated helper classes and factory methods have unique names (i.e. no more than one can use the default name values). Here is an example highlighting method and constructor usage (and also illustrating the renaming required for unique names).
import groovy.transform.builder.*
import groovy.transform.*
@ToString
@Builder
class Person {
String first, last
int born
Person(){}
@Builder(builderClassName='MovieBuilder', builderMethodName='byRoleBuilder')
Person(String roleName) {
if (roleName == 'Jack Sparrow') {
this.first = 'Johnny'; this.last = 'Depp'; this.born = 1963
}
}
@Builder(builderClassName='NameBuilder', builderMethodName='nameBuilder', prefix='having', buildMethodName='fullName')
static String join(String first, String last) {
first + ' ' + last
}
@Builder(builderClassName='SplitBuilder', builderMethodName='splitBuilder')
static Person split(String name, int year) {
def parts = name.split(' ')
new Person(first: parts[0], last: parts[1], born: year)
}
}
assert Person.splitBuilder().name("Johnny Depp").year(1963).build().toString() == 'Person(Johnny, Depp, 1963)'
assert Person.byRoleBuilder().roleName("Jack Sparrow").build().toString()