使用Groovy实现Domain-Specific Languages


1. Command chains 链式调用

Groovy lets you omit parentheses around the arguments of a method call for top-level statements. "command chain" feature extends this by allowing us to chain such parentheses-free method calls, requiring neither parentheses around arguments, nor dots between the chained calls. The general idea is that a call like a b c d will actually be equivalent to a(b).c(d). This also works with multiple arguments, closure arguments, and even named arguments. Furthermore, such command chains can also appear on the right-hand side of assignments. Let’s have a look at some examples supported by this new syntax:

Groovy 允许函数调用的时候不写括号,"链式调用"特性又允许函数调用不写点(.),两者配合,使得函数调用的时候既不需要写括号,又不需要写点(.)。比如说a b c d等价于a(b).c(d)。这个特性同样适用多参数、闭包参数、命名参数的情况。此外,这个调用链也可以放在赋值语句的右边。下面是一些例子:

// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good

// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }

It is also possible to use methods in the chain which take no arguments, but in that case, the parentheses are needed:


// equivalent to: select(all).unique().from(names)
select all unique() from names

If your command chain contains an odd number of elements, the chain will be composed of method / arguments, and will finish by a final property access:


// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

这里就揭示了一个规律,链式调用正常由偶数个元素组成,奇数位置是函数名,偶数位置是参数a b c d等于a(b).c(d),上面那个例子paint wall with red, green and yellowred, green中间是逗号隔开,所以是一个整体,算一个,另外没有参数的时候,括号不能省略,也是为了遵从这一规律。

This command chain approach opens up interesting possibilities in terms of the much wider range of DSLs which can now be written in Groovy.


The above examples illustrate using a command chain based DSL but not how to create one. There are various strategies that you can use, but to illustrate creating such a DSL, we will show a couple of examples – first using maps and Closures:


show = { println it }
square_root = { Math.sqrt(it) }

def please(action) {
  [the: { what ->
    [of: { n -> action(what(n)) }]

// equivalent to: please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0

map的语法可以参考这里,简单来说对于一个map:def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF'],有两种方法获取key的值colors['red']colors.green

As a second example, consider how you might write a DSL for simplifying one of your existing APIs. Maybe you need to put this code in front of customers, business analysts or testers who might be not hard-core Java developers. We’ll use the Splitter from the Google Guava libraries project as it already has a nice Fluent API. Here is how we might use it out of the box:

第二个例子,我们以Google Guava libraries中的Splitter为例子,对于这种已有函数,代码一般会写成这样:

import com.google.common.base.*
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()

It reads fairly well for a Java developer but if that is not your target audience or you have many such statements to write, it could be considered a little verbose. Again, there are many options for writing a DSL. We’ll keep it simple with Maps and Closures. We’ll first write a helper method:

对一个Java程序员来说,这个代码写得挺漂亮,但是有一说一,确实有点冗长。有很多方法可以把他改造成DSL,但是这里我们将以Maps 和 Closures为例来简化他,定义一个帮助函数:

import com.google.common.base.*
def split(string) {
  [on: { sep ->
    [trimming: { trimChar ->
      Splitter.on(sep).trimResults(CharMatcher.is(trimChar as char)).split(string).iterator().toList()

now instead of this line from our original example:


def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()

we can write this:


def result = split "_a ,_b_ ,c__" on ',' trimming '_\'

2. Operator overloading 操作符重载

Various operators in Groovy are mapped onto regular method calls on objects.

This allows you to provide your own Java or Groovy objects which can take advantage of operator overloading. The following table describes the operators supported in Groovy and the methods they map to.

在Groovy语言中,很多操作符都映射到常规的方法调用,这就可以让你自己定义的 Java或者Groovy类也能使用操作符。

Operator Method
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a ** b a.power(b)
a / b a.div(b)
a % b a.mod(b)
a | b a.or(b)
a & b a.and(b)
a ^ b a.xor(b)
a++ or ++a a.next()
a-- or --a a.previous()
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
a << b a.leftShift(b)
a >> b a.rightShift(b)
a >>> b a.rightShiftUnsigned(b)
switch(a) { case(b) : } b.isCase(a)
if(a) a.asBoolean()
~a a.bitwiseNegate()
-a a.negative()
+a a.positive()
a as b a.asType(b)
a == b a.equals(b)
a != b ! a.equals(b)
a <=> b a.compareTo(b)
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a \< b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

3. Script base classes 脚本基类

3.1 The Script class 脚本类

Groovy scripts are always compiled to classes. For example, a script as simple as:


println 'Hello from Groovy'

is compiled to a class extending the abstract groovy.lang.Script class. This class contains a single abstract method called run. When a script is compiled, then its body will become the run method, while the other methods found in the script are found in the implementing class. The Script class provides base support for integration with your application through the Binding object, as illustrated in this example:

当一个脚本被编译的时候,它将被编译成一个继承 groovy.lang.Script (这个类是abstract 的,它有一个虚函数run)的类,脚本的内容就会变成run函数的内容,脚本中别的函数就会变成这个类的成员函数。这个类可以通过Binding对象,在你的应用程序和脚本之间进行交互,比如:

def binding = new Binding()             //1 a binding is used to share data between the script and the calling class
def shell = new GroovyShell(binding)    //2 a GroovyShell can be used with this binding
binding.setVariable('x',1)              //3 input variables are set from the calling class inside the binding
shell.evaluate 'z=2*x+y'                //4 then the script is evaluated
assert binding.getVariable('z') == 5    //5 and the `z` variable has been "exported" into the binding
  1. binding可以用来共享数据
  2. GroovyShell接受binding参数
  3. 通过binding向脚本传递变量
  4. 执行脚本
  5. 脚本可以通过binding暴露数据

This is a very practical way to share data between the caller and the script, however it may be insufficient or not practical in some cases. For that purpose, Groovy allows you to set your own base script class. A base script class has to extend groovy.lang.Script and be a single abstract method type:


abstract class MyBaseClass extends Script {
    String name
    public void greet() { println "Hello, $name!" }

Then the custom script base class can be declared in the compiler configuration, for example:


def config = new CompilerConfiguration()                                //1
config.scriptBaseClass = 'MyBaseClass'                                  //2
def shell = new GroovyShell(this.class.classLoader, config)             //3
shell.evaluate """
    setName 'Judith'                                                    //4
  1. 定义一个CompilerConfiguration
  2. 指定脚本类
  3. 创建GroovyShell
  4. 现在脚本可以直接设置name属性,调用greet函数

3.2. The @BaseScript annotation @BaseScript注解

As an alternative, it is also possible to use the @BaseScript annotation directly into a script:


import groovy.transform.BaseScript

@BaseScript MyBaseClass baseScript
setName 'Judith'

where @BaseScript should annotate a variable which type is the class of the base script. Alternatively, you can set the base script class as a member of the @BaseScript annotation itself:


import groovy.transform.BaseScript

setName 'Judith'



3.3. Alternate abstract method 虚函数的替身

We have seen that the base script class is a single abstract method type that needs to implement the run method. The run method is executed by the script engine automatically. In some circumstances it may be interesting to have a base class which implements the run method, but provides an alternative abstract method to be used for the script body. For example, the base script run method might perform some initialization before the run method is executed. This is possible by doing this:


abstract class MyBaseClass extends Script {
    int count
    abstract void scriptBody()                              //1
    def run() {
        count++                                             //2
        scriptBody()                                        //3
        count                                               //4
  1. 脚本基类重新定义唯一一个虚函数
  2. run函数在运行脚本内容之前可以做点别的事情
  3. run函数调用scriptBody,从而执行脚本类容
  4. 返回自定义内容,而不是脚本的内容

If you execute this code:

def result = shell.evaluate """
    println 'Ok'
assert result == 1

Then you will see that the script is executed, but the result of the evaluation is 1 as returned by the run method of the base class. It is even clearer if you use parse instead of evaluate, because it would allow you to execute the run method several times on the same script instance:

def script = shell.parse("println 'Ok'")
assert script.run() == 1
assert script.run() == 2


4. Adding properties to numbers 给数字添加属性

In Groovy number types are considered equal to any other types. As such, it is possible to enhance numbers by adding properties or methods to them. This can be very handy when dealing with measurable quantities for example. Details about how existing classes can be enhanced in Groovy are found in the extension modules section or the categories section.

在Groovy语言中,数字类型和其它所有类型地位相同,因此,可以通过为数字添加属性或者函数,来对数字类型进行增强。这个在你处理一个可测量的数据是非常有用。关于增强一个已有类型的内容可以参考这两个地方 extension modules section or the categories section。

An illustration of this can be found in Groovy using the TimeCategory:

下面以 TimeCategory为例:

use(TimeCategory)  {
    println 1.minute.from.now       //1
    println 10.hours.ago

    def someDate = new Date()       //2
    println someDate - 3.months
  1. 通过TimeCategory,给Integer添加了一个minute属性。
  2. 类似的,months函数返回一个 groovy.time.DatumDependentDuration类型,支持计算。

Categories are lexically bound, making them a great fit for internal DSLs.


5. @DelegatesTo

5.1. Explaining delegation strategy at compile time 什么是编译时委托策略

@groovy.lang.DelegatesTo is a documentation and compile-time annotation aimed at:

  • documenting APIs that use closures as arguments
  • providing type information for the static type checker and compiler

@groovy.lang.DelegatesTo 是一个注解,它的主要用途是:

  • 在将一个闭包作为参数使用时,记录它的API
  • 为静态类型检查以及编译器提供类型信息

The Groovy language is a platform of choice for building DSLs. Using closures, it’s quite easy to create custom control structures, as well as it is simple to create builders. Imagine that you have the following code:

Groovy 语言是构建 DSL 的首选平台。 使用闭包,可以很容易创建自定义控制结构,也很容易创建构建器。假设您有以下代码:

email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'

One way of implementing this is using the builder strategy, which implies a method, named email which accepts a closure as an argument. The method may delegate subsequent calls to an object that implements the from, to, subject and body methods. Again, body is a method which accepts a closure as an argument and that uses the builder strategy.


Implementing such a builder is usually done the following way:


def email(Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY

the EmailSpec class implements the from, to, … methods. By calling rehydrate, we’re creating a copy of the closure for which we set the delegate, owner and thisObject values. Setting the owner and the this object is not very important here since we will use the DELEGATE_ONLY strategy which says that the method calls will be resolved only against the delegate of the closure.

首先EmailSpec类实现了 from, to这些函数。接着,通过rehydrate函数,我们创建了原始闭包的一个副本,三个参数分别是: delegate, owner and thisObject,在这个例子中,后面两个参数不重要,只是随便赋个值,因为在第三行我们设置了 DELEGATE_ONLY 策略,它的效果是闭包里面的那些函数统统都在email中找:

class EmailSpec {
    void from(String from) { println "From: $from"}
    void to(String... to) { println "To: $to"}
    void subject(String subject) { println "Subject: $subject"}
    void body(Closure body) {
        def bodySpec = new BodySpec()
        def code = body.rehydrate(bodySpec, this, this)
        code.resolveStrategy = Closure.DELEGATE_ONLY

The EmailSpec class has itself a body method accepting a closure that is cloned and executed. This is what we call the builder pattern in Groovy.

EmailSpec 类本身有一个 body 函数,该函数接受闭包。 这就是我们在 Groovy 中所说的构建器模式。

One of the problems with the code that we’ve shown is that the user of the email method doesn’t have any information about the methods that he’s allowed to call inside the closure. The only possible information is from the method documentation. There are two issues with this: first of all, documentation is not always written, and if it is, it’s not always available (javadoc not downloaded, for example). Second, it doesn’t help IDEs. What would be really interesting, here, is for IDEs to help the developer by suggesting, once they are in the closure body, methods that exist on the email class.


Moreover, if the user calls a method in the closure which is not defined by the EmailSpec class, the IDE should at least issue a warning (because it’s very likely that it will break at runtime).

此外,如果用户在闭包中调用了 EmailSpec 类未定义的方法,IDE 至少应该发出警告(因为它很可能会在运行时中断)。

One more problem with the code above is that it is not compatible with static type checking. Type checking would let the user know if a method call is authorized at compile time instead of runtime, but if you try to perform type checking on this code:


email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'

Then the type checker will know that there’s an email method accepting a Closure, but it will complain for every method call inside the closure, because from, for example, is not a method which is defined in the class. Indeed, it’s defined in the EmailSpec class and it has absolutely no hint to help it knowing that the closure delegate will, at runtime, be of type EmailSpec:


void sendEmail() {
    email {
        from 'dsl-guru@mycompany.com'
        to 'john.doe@waitaminute.com'
        subject 'The pope has resigned!'
        body {
            p 'Really, the pope has resigned!'

will fail compilation with errors like this one:


[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 31, column 21.
                       from 'dsl-guru@mycompany.com'

5.2. @DelegatesTo

For those reasons, Groovy 2.1 introduced a new annotation named @DelegatesTo. The goal of this annotation is to solve both the documentation issue, that will let your IDE know about the expected methods in the closure body, and it will also solve the type checking issue, by giving hints to the compiler about what are the potential receivers of method calls in the closure body.

为了解决这个问题,Groovy 2.1引入了一个新注解@DelegatesTo

The idea is to annotate the Closure parameter of the email method:


def email(@DelegatesTo(EmailSpec) Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY

What we’ve done here is telling the compiler (or the IDE) that when the method will be called with a closure, the delegate of this closure will be set to an object of type email. But there is still a problem: the default delegation strategy is not the one which is used in our method. So we will give more information and tell the compiler (or the IDE) that the delegation strategy is also changed:


def email(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=EmailSpec) Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY

Now, both the IDE and the type checker (if you are using @TypeChecked) will be aware of the delegate and the delegation strategy. This is very nice because it will both allow the IDE to provide smart completion, but it will also remove errors at compile time that exist only because the behaviour of the program is normally only known at runtime!


The following code will now pass compilation:

void doEmail() {
    email {
        from 'dsl-guru@mycompany.com'
        to 'john.doe@waitaminute.com'
        subject 'The pope has resigned!'
        body {
            p 'Really, the pope has resigned!'

5.3. DelegatesTo modes 代理模式

@DelegatesTo supports multiple modes that we will describe with examples in this section.


5.3.1. Simple delegation 简单代理

In this mode, the only mandatory parameter is the value which says to which class we delegate calls. Nothing more. We’re telling the compiler that the type of the delegate will always be of the type documented by @DelegatesTo (note that it can be a subclass, but if it is, the methods defined by the subclass will not be visible to the type checker).



void body(@DelegatesTo(BodySpec) Closure cl) {
    // ...

5.3.2. Delegation strategy 代理策略

In this mode, you must specify both the delegate class and a delegation strategy. This must be used if the closure will not be called with the default delegation strategy, which is Closure.OWNER_FIRST.


void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
    // ...

5.3.3. Delegate to parameter 代理变量

In this variant, we will tell the compiler that we are delegating to another parameter of the method. Take the following code:



def exec(Object target, Closure code) {
   def clone = code.rehydrate(target, this, this)

Here, the delegate which will be used is not created inside the exec method. In fact, we take an argument of the method and delegate to it. Usage may look like this:


def email = new Email()
exec(email) {
   from '...'
   to '...'

Each of the method calls are delegated to the email parameter. This is a widely used pattern which is also supported by @DelegatesTo using a companion annotation:


def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
   def clone = code.rehydrate(target, this, this)

A closure is annotated with @DelegatesTo, but this time, without specifying any class. Instead, we’re annotating another parameter with @DelegatesTo.Target. The type of the delegate is then determined at compile time. One could think that we are using the parameter type, which in this case is Object but this is not true. Take this code:

closure还是用@DelegatesTo标注,但是另外参数使用 @DelegatesTo.Target注解,这样就行了:

class Greeter {
   void sayHello() { println 'Hello' }
def greeter = new Greeter()
exec(greeter) {

Remember that this works out of the box without having to annotate with @DelegatesTo. However, to make the IDE aware of the delegate type, or the type checker aware of it, we need to add @DelegatesTo. And in this case, it will know that the Greeter variable is of type Greeter, so it will not report errors on the sayHello method even if the exec method doesn’t explicitly define the target as of type Greeter. This is a very powerful feature, because it prevents you from writing multiple versions of the same exec method for different receiver types!

其实这里不用 @DelegatesTo标注也是可以的,但是这里加上 @DelegatesTo可以给IDE提供额外的信息,这真是一个非常牛逼特性。

In this mode, the @DelegatesTo annotation also supports the strategy parameter that we’ve described upper.

在这个模式中, @DelegatesTo 注解依然支持 strategy

5.3.4. Multiple closures 多个闭包的情况

In the previous example, the exec method accepted only one closure, but you may have methods that take multiple closures:


void fooBarBaz(Closure foo, Closure bar, Closure baz) {

Then nothing prevents you from annotating each closure with @DelegatesTo:


class Foo { void foo(String msg) { println "Foo ${msg}!" } }
class Bar { void bar(int x) { println "Bar ${x}!" } }
class Baz { void baz(Date d) { println "Baz ${d}!" } }

void fooBarBaz(@DelegatesTo(Foo) Closure foo, @DelegatesTo(Bar) Closure bar, @DelegatesTo(Baz) Closure baz) {

But more importantly, if you have multiple closures and multiple arguments, you can use several targets:


void fooBarBaz(
    @DelegatesTo.Target('foo') foo,
    @DelegatesTo.Target('bar') bar,
    @DelegatesTo.Target('baz') baz,

    @DelegatesTo(target='foo') Closure cl1,
    @DelegatesTo(target='bar') Closure cl2,
    @DelegatesTo(target='baz') Closure cl3) {
    cl1.rehydrate(foo, this, this).call()
    cl2.rehydrate(bar, this, this).call()
    cl3.rehydrate(baz, this, this).call()

def a = new Foo()
def b = new Bar()
def c = new Baz()
    a, b, c,
    { foo('Hello') },
    { bar(123) },
    { baz(new Date()) }

At this point, you may wonder why we don’t use the parameter names as references. The reason is that the information (the parameter name) is not always available (it’s a debug-only information), so it’s a limitation of the JVM.


5.3.5. Delegating to a generic type 对泛型的代理

In some situations, it is interesting to instruct the IDE or the compiler that the delegate type will not be a parameter but a generic type. Imagine a configurator that runs on a list of elements:



public <T> void configure(List<T> elements, Closure configuration) {
   elements.each { e->
      def clone = configuration.rehydrate(e, this, this)
      clone.resolveStrategy = Closure.DELEGATE_FIRST

Then this method can be called with any list like this:


class Realm {
   String name
List<Realm> list = []
3.times { list << new Realm() }
configure(list) {
   name = 'My Realm'
assert list.every { it.name == 'My Realm' }

To let the type checker and the IDE know that the configure method calls the closure on each element of the list, you need to use @DelegatesTo differently:

public <T> void configure(
    @DelegatesTo.Target List<T> elements,
    @DelegatesTo(strategy=Closure.DELEGATE_FIRST, genericTypeIndex=0) Closure configuration) {
   def clone = configuration.rehydrate(e, this, this)
   clone.resolveStrategy = Closure.DELEGATE_FIRST

@DelegatesTo takes an optional genericTypeIndex argument that tells what is the index of the generic type that will be used as the delegate type. This must be used in conjunction with @DelegatesTo.Target and the index starts at 0. In the example above, that means that the delegate type is resolved against List<T>, and since the generic type at index 0 is T and inferred as a Realm, the type checker infers that the delegate type will be of type Realm.


We’re using a genericTypeIndex instead of a placeholder (T) because of JVM limitations.


5.3.6. Delegating to an arbitrary type 代理任意的类型


It is possible that none of the options above can represent the type you want to delegate to. For example, let’s define a mapper class which is parametrized with an object and defines a map method which returns an object of another type:

class Mapper<T,U> {                             
    final T value                               
    Mapper(T value) { this.value = value }
    U map(Closure<U> producer) {                
        producer.delegate = value
The mapper class takes two generic type arguments: the source type and the target type
The source object is stored in a final field
The map method asks to convert the source object to a target object

As you can see, the method signature from map does not give any information about what object will be manipulated by the closure. Reading the method body, we know that it will be the value which is of type T, but T is not found in the method signature, so we are facing a case where none of the available options for @DelegatesTo is suitable. For example, if we try to statically compile this code:

def mapper = new Mapper<String,Integer>('Hello')
assert mapper.map { length() } == 5

Then the compiler will fail with:

Static type checking] - Cannot find matching method TestScript0#length()

In that case, you can use the type member of the @DelegatesTo annotation to reference T as a type token:

class Mapper<T,U> {
    final T value
    Mapper(T value) { this.value = value }
    U map(@DelegatesTo(type="T") Closure<U> producer) {  
        producer.delegate = value
The @DelegatesTo annotation references a generic type which is not found in the method signature

Note that you are not limited to generic type tokens. The type member can be used to represent complex types, such as List<T> or Map<T,List<U>>. The reason why you should use that in last resort is that the type is only checked when the type checker finds usage of @DelegatesTo, not when the annotated method itself is compiled. This means that type safety is only ensured at the call site. Additionally, compilation will be slower (though probably unnoticeable for most cases).

Leave a Comment

Back to Top