Groovy Goodness: Add Map Constructor With Annotation
Since the early days of Groovy we can create POGO (Plain Old Groovy Objects) classes that will have a constructor with a Map
argument.
Groovy adds the constructor automatically in the generated class.
We can use named arguments to create an instance of a POGO, because of the Map
argument constructor.
This only works if we don’t add our own constructor and the properties are not final.
Since Groovy 2.5.0 we can use the @MapConstrutor
AST transformation annotation to add a constructor with a Map
argument.
Using the annotation we can have more options to customize the generated constructor.
We can for example let Groovy generate the constructor with Map
argument and add our own constructor.
Also properties can be final and we can still use a constructor with Map
argument.
First we look at the default behaviour in Groovy when we create a POGO:
// Simple POGO.
// Groovy adds Map argument
// constructor to the class.
class Person {
String name
String alias
List likes
}
// Create Person object using
// the Map argument constructor.
// We can use named arguments,
// with the name of key being
// the property name. Groovy
// converts this to Map.
def mrhaki =
new Person(
alias: 'mrhaki',
name: 'Hubert Klein Ikkink',
likes: ['Groovy', 'Gradle'])
assert mrhaki.alias == 'mrhaki'
assert mrhaki.name == 'Hubert Klein Ikkink'
assert mrhaki.likes == ['Groovy', 'Gradle']
// Sample class with already
// a constructor. Groovy cannot
// create a Map argument constructor now.
class Student {
String name
String alias
Student(String name) {
this.name = name
}
}
import static groovy.test.GroovyAssert.shouldFail
// When we try to use named arguments (turns into a Map)
// in the constructor we get an exception.
def exception = shouldFail(GroovyRuntimeException) {
def student =
new Student(
name: 'Hubert Klein Ikkink',
alias: 'mrhaki')
}
assert exception.message.startsWith('failed to invoke constructor: public Student(java.lang.String) with arguments: []')
assert exception.message.endsWith('reason: java.lang.IllegalArgumentException: wrong number of arguments')
Now let’s use the @MapConstructor
annotation in our next example:
import groovy.transform.MapConstructor
@MapConstructor
class Person {
final String name // AST transformation supports read-only properties.
final String alias
List likes
}
// Create object using the Map argument constructor.
def mrhaki =
new Person(
name: 'Hubert Klein Ikkink',
alias: 'mrhaki',
likes: ['Groovy', 'Gradle'])
assert mrhaki.name == 'Hubert Klein Ikkink'
assert mrhaki.alias == 'mrhaki'
assert mrhaki.likes == ['Groovy', 'Gradle']
// Using the annotation the Map argument
// constructor is added, even though we
// have our own constructor as well.
@MapConstructor
class Student {
String name
String alias
Student(String name) {
this.name = name
}
}
def student =
new Student(
name: 'Hubert Klein Ikkink',
alias: 'mrhaki')
assert student.name == 'Hubert Klein Ikkink'
assert student.alias == 'mrhaki'
The AST transformation supports several attributes.
We can use the attributes includes
and excludes
to include or exclude properties that will get a value in the Map
argument constructor.
In the following example we see how we can use the includes
attribute:
import groovy.transform.MapConstructor
@MapConstructor(includes = 'name')
class Person {
final String name
final String alias
List likes
}
// Create object using the Map argument constructor.
def mrhaki =
new Person(
name: 'Hubert Klein Ikkink',
alias: 'mrhaki',
likes: ['Groovy', 'Gradle'])
assert mrhaki.name == 'Hubert Klein Ikkink'
assert !mrhaki.alias
assert !mrhaki.likes
We can add custom code that is executed before or after the generated code by the AST transformation using the attributes pre
and post
.
We assign a Closure
to these attributes with the code that needs to be executed.
In the next example we set the pre
attribute with code that calculates the alias
property value if it is not set via the constructor:
// If alias is set in constructor use it, otherwise
// calculate alias value based on name value.
@MapConstructor(post = { alias = alias ?: name.split().collect { it[0] }.join() })
class Person {
final String name // AST transformation supports read-only properties.
final String alias
List likes
}
// Set alias in constructor.
def mrhaki =
new Person(
name: 'Hubert Klein Ikkink',
alias: 'mrhaki',
likes: ['Groovy', 'Gradle'])
assert mrhaki.name == 'Hubert Klein Ikkink'
assert mrhaki.alias == 'mrhaki'
assert mrhaki.likes == ['Groovy', 'Gradle']
// Don't set alias via constructor.
def hubert =
new Person(
name: 'Hubert A. Klein Ikkink')
assert hubert.name == 'Hubert A. Klein Ikkink'
assert hubert.alias == 'HAKI'
assert !hubert.likes
Written with Groovy 2.5.0.