鍍金池/ 教程/ Java/ 領(lǐng)域?qū)S谜Z(yǔ)言
Grape 依賴(lài)管理器
與 Java 的區(qū)別
語(yǔ)法風(fēng)格指南
Groovy 開(kāi)發(fā)工具包
領(lǐng)域?qū)S谜Z(yǔ)言
安全更新
Groovy 與應(yīng)用的集成
運(yùn)行時(shí)及編譯時(shí)元編程(end)
測(cè)試指南
安裝 Groovy
設(shè)計(jì)模式
Groovy 的下載

領(lǐng)域?qū)S谜Z(yǔ)言

1 命令鏈

Groovy 可以使你省略頂級(jí)語(yǔ)句方法調(diào)用中參數(shù)外面的括號(hào)。“命令鏈”功能則將這種特性繼續(xù)擴(kuò)展,它可以將不需要括號(hào)的方法調(diào)用串接成鏈,既不需要參數(shù)周?chē)睦ㄌ?hào),鏈接的調(diào)用之間也不需要點(diǎn)號(hào)。舉例來(lái)說(shuō),a b c d 實(shí)際上就等同于 a(b).c(d)。它適用于多個(gè)參數(shù)、閉包參數(shù),甚至命名參數(shù)。而且,這樣的命令鏈也可以出現(xiàn)在賦值的右方。讓我們來(lái)看看應(yīng)用這一新的語(yǔ)法格式的范例:

// 等同于:turn(left).then(right)
turn left then right

// 等同于:take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// 等同于:paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// 命名參數(shù)
// 等同于:check(that: margarita).tastes(good)
check that: margarita tastes good

// 閉包作為參數(shù)
// 等同于:given({}).when({}).then({})
given { } when { } then { }

不帶參數(shù)的鏈中也可以使用方法,但在這種情況下需要用括號(hào)。

// 等同于:select(all).unique().from(names)
select all unique() from names

如果命令鏈包含奇數(shù)個(gè)元素,鏈會(huì)由方法和參數(shù)組成,最終由一個(gè)最終屬性訪(fǎng)問(wèn):

// 等同于:take(3).cookies
// 同樣也等于:take(3).getCookies()
take 3 cookies

借助命令鏈方法有趣的一點(diǎn)是,我們能夠利用 Groovy 編寫(xiě)出更多的 DSL。

上面的范例展示了使用基于 DSL 的命令鏈,但是不知道如何創(chuàng)建一個(gè)。你可以使用很多策略,但是為了展示如何創(chuàng)建 DSL,下面列舉一些范例,首先使用 map 映射與閉包:

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

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

// 等同于:please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0

第二個(gè)范例要考慮如何編寫(xiě) DSL 來(lái)簡(jiǎn)化一個(gè)現(xiàn)存的 API?;蛟S你需要把這些代碼展示給顧客、業(yè)務(wù)分析師或測(cè)試員,有可能這些人并不是技藝非常精湛的 Java 開(kāi)發(fā)者。我們將使用 Splitter,一個(gè)Google 的 Guava libraries 項(xiàng)目,因?yàn)樗呀?jīng)是一個(gè)良好的 Fluent API 了。下面展示如何使用它。

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

對(duì)于 Java 開(kāi)發(fā)者而言,這段代碼很容易明白,但如果面對(duì)的是目標(biāo)顧客,或者要寫(xiě)很多這樣的語(yǔ)句,那就可能顯得有些啰嗦了。再次提醒一下,DSL 的編寫(xiě)手段多種多樣。利用映射和閉包來(lái)將它簡(jiǎn)化一下,首先編寫(xiě)一個(gè)輔助函數(shù):

@Grab('com.google.guava:guava:r09')
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()
    }]
  }]
}

然后找到原始范例中的這一行:

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

替換成下面這行:

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

2. 操作符重載

Groovy 的多種操作符都可以被映射到對(duì)象的正則方法調(diào)用上。

這允許你提供自己的 Java 或 Groovy 對(duì)象,以便利用操作符重載這一優(yōu)點(diǎn)。下面這張表展示了 Groovy 支持的操作符以及其映射的方法。

操作符 方法
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++++a a.next()
a----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. 腳本基類(lèi)

3.1 Script 類(lèi)

Groovy 腳本經(jīng)常被編譯為類(lèi)。比如下面這個(gè)簡(jiǎn)單的腳本:

println 'Hello from Groovy'

會(huì)被編譯擴(kuò)展自 groovy.lang.Script 抽象類(lèi)的類(lèi)。該類(lèi)只包含一個(gè)抽象方法:run。當(dāng)腳本編譯時(shí),語(yǔ)句體就會(huì)成為 run 方法,腳本中的其他方法都位于實(shí)現(xiàn)類(lèi)中。Script 類(lèi)為通過(guò) Binding 集成應(yīng)用程序提供了基本支持。如下例所示:

def binding = new Binding()                       1??       
def shell = new GroovyShell(binding)              2??   
binding.setVariable('x',1)                        3??   
binding.setVariable('y',3)
shell.evaluate 'z=2*x+y'                          4??   
assert binding.getVariable('z') == 5              5??   

1?? 被用于在腳本和調(diào)用類(lèi)間共享數(shù)據(jù)的綁定對(duì)象。
2?? 與該綁定對(duì)象聯(lián)合使用的 GroovyShell。
3?? 輸入變量從位于綁定對(duì)象內(nèi)部的調(diào)用類(lèi)進(jìn)行設(shè)置。
4?? 然后計(jì)算腳本。
5?? z 變量導(dǎo)出到綁定對(duì)象中。

在調(diào)用者和腳本間共享數(shù)據(jù)是一種很使用的方式,但在有些情況下卻未必有較高的效率或?qū)嵱眯?。為了?yīng)付那種情況,Groovy 允許我們?cè)O(shè)置自己的基本腳本類(lèi)。腳本基類(lèi)必須擴(kuò)展自 groovy.lang.Script,屬于一個(gè)單獨(dú)的抽象方法類(lèi)別。

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

自定義腳本基類(lèi)可以在編譯器配置中聲明,如下所示:

def config = new CompilerConfiguration()                             1??                             
config.scriptBaseClass = 'MyBaseClass'                               2?? 
def shell = new GroovyShell(this.class.classLoader, config)          3??   
shell.evaluate """
    setName 'Judith'                                                 4??   
    greet()
"""

1?? 創(chuàng)建一個(gè)自定義的編譯器配置。
2?? 將腳本基類(lèi)設(shè)為我們自定義的腳本基類(lèi)。
3?? 然后創(chuàng)建一個(gè)使用該配置的 GroovyShell。 4?? 腳本然后擴(kuò)展該腳本基類(lèi),提供對(duì) name 屬性及 greet 方法的直接訪(fǎng)問(wèn)。

3.2 @BaseScript 注釋

在腳本中直接使用 @BaseScript 注釋也是個(gè)不錯(cuò)的辦法:

import groovy.transform.BaseScript

@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()

上例中,通過(guò) @BaseScript 的注釋?zhuān)兞款?lèi)型指定為腳本基類(lèi)。另一種方法是設(shè)置基類(lèi)是 @BaseScript 注釋的成員:

@BaseScript(MyBaseClass)
import groovy.transform.BaseScript

setName 'Judith'
greet()

3.3 替代的抽象方法

由前面的幾個(gè)例子看到,腳本基類(lèi)屬于一種單獨(dú)的抽象方法類(lèi)型,需要實(shí)現(xiàn) run 方法。run 方法由腳本引擎自動(dòng)執(zhí)行。在有些情況下,可能會(huì)由基類(lèi)實(shí)現(xiàn) run 方法,而提供了另外一種抽象方法用于腳本體,這是比較有意思的一種做法。例如,腳本基類(lèi)的 run 方法會(huì)在執(zhí)行 run 方法之前執(zhí)行一些初始化,如下所示:

abstract class MyBaseClass extends Script {
    int count
    abstract void scriptBody()                   1??                        
    def run() {                                  
        count++                                  2??           
        scriptBody()                             3??           
        count                                    4??           
    }
}

1?? 腳本基類(lèi)定義了一個(gè)(只有一個(gè))抽象方法。 2?? run 方法可以被重寫(xiě),并在執(zhí)行腳本體之前執(zhí)行某個(gè)任務(wù)。
3?? run 調(diào)用抽象的 scriptBody 方法,后者委托給用戶(hù)腳本。 4?? 然后它能返回除了腳本值之外的其他內(nèi)容。

如果執(zhí)行下列代碼:

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

你就會(huì)看到腳本被執(zhí)行,但計(jì)算結(jié)果是由基類(lèi)的 run 方法所返回的 1。如果使用 parse 代替 evaluate,你就會(huì)更清楚,因?yàn)槟銜?huì)在同一腳本實(shí)例上執(zhí)行多次 run 方法。

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

4 為數(shù)值添加屬性

在 Groovy 中,數(shù)值類(lèi)型是等同于其他類(lèi)型的。因此我們可以通過(guò)添加屬性或方法來(lái)增強(qiáng)它們的功能。在處理可測(cè)量的量時(shí),這樣做尤為方便。關(guān)于如何增強(qiáng) Groovy 中已有的類(lèi)的詳細(xì)情況,可參見(jiàn)extension modulescategories。

使用 TimeCategory 可展示 Groovy 中的一個(gè)范例:

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

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

1?? 使用 TimeCategory,屬性 minute 添加到 Integer 類(lèi)上。
2?? 同樣,months 方法也將返回一個(gè)用于計(jì)算的 groovy.time.DatumDependentDuration。

類(lèi)別有詞法綁定,所以非常適合內(nèi)部 DSL。

5. @DelegatesTo

5.1. 編譯時(shí)解釋委托策略

@groovy.lang.DelegatesTo 是一個(gè)文檔與編譯時(shí)注釋?zhuān)闹饕饔迷谟冢?

  • 記錄使用閉包做為參數(shù)的 API。
  • 為靜態(tài)類(lèi)型檢查器與編譯器提供類(lèi)型信息。

Groovy 是構(gòu)建 DSL 的一種選擇平臺(tái)。使用閉包可以非常輕松地創(chuàng)建自定義控制結(jié)構(gòu),創(chuàng)建構(gòu)建者也非常方便。比如有下面這樣的代碼:

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

使用構(gòu)建者策略可實(shí)現(xiàn),利用一個(gè)參數(shù)為閉包的名為 email 的方法,它會(huì)將隨后的調(diào)用委托給一個(gè)對(duì)象,該對(duì)象實(shí)現(xiàn)了 from、tosubjectbody 各方法。body 方法使用閉包做參數(shù),使用的是構(gòu)建者策略。

實(shí)現(xiàn)這樣的構(gòu)建者往往要通過(guò)下面的方式:

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

EmailSpec 類(lèi)實(shí)現(xiàn)了 from、to 等方法,通過(guò)調(diào)用 rehydrate,創(chuàng)建了一個(gè)閉包副本,用于為該副本設(shè)置 delegateownerthisObject 等值。設(shè)置 ownerthisObject 并不十分重要,因?yàn)閷⑹褂?DELEGATE_ONLY 策略,解決方法調(diào)用只針對(duì)的是閉包委托。

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
        code()
    }
}

The EmailSpec 類(lèi)自身的 body 方法將接受一個(gè)復(fù)制并執(zhí)行的閉包,這就是 Groovy 構(gòu)建者模式的原理。

代碼中的一個(gè)問(wèn)題在于,email 方法的用戶(hù)并不知道他能在閉包內(nèi)調(diào)用的方法。唯一的了解途徑大概就是方法文檔。不過(guò)這樣也存在兩個(gè)問(wèn)題:首先,有些內(nèi)容并不一定會(huì)寫(xiě)出文檔,如果記下文檔,人們也不一定總能獲得(比如 Javadoc 就是在線(xiàn)的,沒(méi)有下載版本);其二,也沒(méi)有幫助 IDE。假如有 IDE 來(lái)輔助開(kāi)發(fā)者,比如一旦當(dāng)他們?cè)陂]包體內(nèi),就建議采用 email 類(lèi)中的方法。

如果用戶(hù)調(diào)用了閉包內(nèi)的一個(gè)方法,該方法沒(méi)有被 EmailSpec 類(lèi)所定義,IDE 應(yīng)該至少能提供一個(gè)警告(因?yàn)檫@非常有可能會(huì)在運(yùn)行時(shí)造成崩潰)。

上面代碼還存在的一個(gè)問(wèn)題是,它與靜態(tài)類(lèi)型檢查不兼容。類(lèi)型檢查會(huì)讓用戶(hù)了解方法調(diào)用是否在編譯時(shí)被授權(quán)(而不是在運(yùn)行時(shí)),但如果你對(duì)下面這種代碼執(zhí)行類(lèi)型檢查的話(huà):

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

類(lèi)型檢查器當(dāng)然知道存在一個(gè)接收 Closureemail 方法,但是它會(huì)為閉包內(nèi)的每個(gè)方法都進(jìn)行解釋?zhuān)热缯f(shuō) from,它不是一個(gè)定義在類(lèi)中的方法,實(shí)際上它定義在 EmailSpec 類(lèi)中,但在運(yùn)行時(shí),沒(méi)有任何線(xiàn)索能讓檢查器知道它的閉包委托類(lèi)型是 EmailSpec

@groovy.transform.TypeChecked
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!'
        }
    }
}

所以在編譯時(shí)會(huì)失敗,錯(cuò)誤信息如下:

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

5.2. @DelegatesTo

基于以上這些原因,Groovy 2.1 引入了一個(gè)新的注釋?zhuān)?code>@DelegatesTo。該注釋目的在于解決文檔問(wèn)題,讓 IDE 了解閉包體內(nèi)的期望方法。同時(shí),它還能夠給編譯器提供一些提示,告知編譯器閉包體內(nèi)的方法調(diào)用的可能接收者是誰(shuí),從而解決類(lèi)型檢查問(wèn)題。

具體方法是注釋 email 方法中的 Closure 參數(shù):

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

上面代碼告訴編譯器(或者 IDE)閉包內(nèi)的方法何時(shí)被調(diào)用,閉包委托被設(shè)置為 email 類(lèi)型對(duì)象。但這里仍遺漏了一個(gè)問(wèn)題:默認(rèn)的委托策略并非方法所使用的那一種。因此,我們還需要提供更多信息,告訴編譯器(或 IDE)委托策略也改變了。

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
    code()
}

現(xiàn)在,IDE 和類(lèi)型檢查器(如果使用 @TypeChecked)都能知道委托和委托策略了?,F(xiàn)在,IDE 不僅可以進(jìn)行智能補(bǔ)足,而且還能消除編譯時(shí)出現(xiàn)的錯(cuò)誤,而這種錯(cuò)誤的產(chǎn)生,通常只是因?yàn)槌绦蛐袨橹挥械搅诉\(yùn)行時(shí)才被知曉。

下面的代碼編譯起來(lái)沒(méi)有任何問(wèn)題了:


@TypeChecked
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 模式

@DelegatesTo 支持多種模式,本部分內(nèi)容將予以詳細(xì)介紹。

5.3.1. 簡(jiǎn)單委托

該模式中唯一強(qiáng)制的參數(shù)是 value,它指明了委托調(diào)用的類(lèi)。除此之外沒(méi)有別的。編譯器還將知道:委托類(lèi)型將一直是由 @DelegatesTo 所記錄的類(lèi)型。(注意它可能是個(gè)子類(lèi),如果是子類(lèi)的話(huà),對(duì)于類(lèi)型檢查器來(lái)說(shuō),由該子類(lèi)所定義的方法是可見(jiàn)的。)

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

5.3.2. 委托策略

在該模式中,必須指定委托類(lèi)和委托策略。如果閉包沒(méi)有以缺省的委托策略(Closure.OWNER_FIRST)進(jìn)行調(diào)用,就必須使用該模式。

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

5.3.3. 委托給參數(shù)

在這種形式中,我們將會(huì)告訴編譯器將委托給方法的另一個(gè)參數(shù)。如下所示:

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

這里所用的委托不是在 exec 方法內(nèi)創(chuàng)建的。實(shí)際上是拿了方法中的一個(gè)參數(shù)然后委托給它。如下所示:

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

每個(gè)方法調(diào)用都委托給了 email 參數(shù)。這是一種應(yīng)用很廣的模式,它也能被使用聯(lián)合注釋 @DelegatesTo 所支持。

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

閉包使用了注釋 @DelegatesTo,但這一次,沒(méi)有指定任何類(lèi),而利用 @DelegatesTo.Target 注釋了另一個(gè)參數(shù)。委托類(lèi)型在編譯時(shí)進(jìn)行指定??赡苡腥藭?huì)認(rèn)為使用參數(shù)類(lèi)型,比如在該例中是 Object,但這是錯(cuò)的。代碼如下:

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

注意,這不需要利用 @DelegatesTo 注釋。但是,要想讓 IDE 或者類(lèi)型檢查器知道委托類(lèi)型,我們需要 @DelegatesTo。本例中,Greeter 變量屬于 Greeter 類(lèi)型,而且即使 exec 方法并沒(méi)有明顯地定義 Greeter 類(lèi)型的目標(biāo),sayHello 方法也不會(huì)報(bào)出錯(cuò)誤。這種功能非常有用,可以避免我們針對(duì)不同的接收類(lèi)型而編寫(xiě)不同的 exec 方法。

該模式下,@DelegatesTo 注釋也支持我們上面介紹的 strategy 參數(shù)。

5.3.4 多個(gè)閉包

前例中,exec 方法只接受一個(gè)閉包,但是可能會(huì)有接收多個(gè)閉包的方法:

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

利用 @DelegatesTo 注釋每個(gè)閉包就顯得不可避免了:

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 $ks04ke4!" } }

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

但更重要的是,如果有多個(gè)閉包和多個(gè)參數(shù),可以使用一些目標(biāo):

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()
fooBarBaz(
    a, b, c,
    { foo('Hello') },
    { bar(123) },
    { baz(new Date()) }
)

這時(shí),你可能會(huì)感到困惑:為何我們不把引用作為參數(shù)名呢?因?yàn)橄嚓P(guān)信息(參數(shù)名)不一定能獲取到(只供調(diào)試的信息),所以這是 JVM 的一個(gè)缺陷。

5.3.5. 委托給基本類(lèi)型

在一些情況下,可以命令 IDE 或編譯器,使委托類(lèi)型不是參數(shù)而是某種基本類(lèi)型。假設(shè)有下面這樣運(yùn)行在一列元素上的配置器:

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

然后利用任何列表都可以調(diào)用該方法:

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

要想讓類(lèi)型檢查器和 IDE 了解 configure 方法在列表的每個(gè)元素上調(diào)用閉包,你需要換一種方式來(lái)使用 @DelegatesTo

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
   clone.call()
}

@DelegatesTo 獲取一個(gè)可選的 genericTypeIndex 參數(shù),該參數(shù)指出被用作委托類(lèi)型的基本類(lèi)型的具體索引。它必須要與 @DelegatesTo.Target 聯(lián)合使用,并且起始索引要為 0。在上例中,委托類(lèi)型會(huì)根據(jù) List<T> 來(lái)判定,因?yàn)樵谒饕?0 處的基本類(lèi)型是 T,并推斷是 Realm,所以類(lèi)型檢查器也會(huì)推斷委托類(lèi)型屬于 Realm 類(lèi)型。

由于 JVM 的限制,我們使用 genericTypeIndex 來(lái)代替占位符(T)。

5.3.6. 委托給任意類(lèi)型

有可能上述所有方式都無(wú)法表示你想要委托的類(lèi)型。比如,可以定義一個(gè) Mapper 類(lèi),它帶有一個(gè)對(duì)象參數(shù),并且定義了一個(gè)能夠返回其他類(lèi)型對(duì)象的 map 方法:

class Mapper<T,U> {                             1??
    final T value                               2??
    Mapper(T value) { this.value = value }         
    U map(Closure<U> producer) {                3??
        producer.delegate = value
        producer()
    }
}

1?? Mapper 類(lèi)接受兩個(gè)通用類(lèi)型參數(shù):源類(lèi)型與目標(biāo)類(lèi)型。
2?? 源對(duì)象保存在一個(gè) final 類(lèi)型的對(duì)象中。
3?? map 方法請(qǐng)求將源對(duì)象轉(zhuǎn)換為目標(biāo)對(duì)象。

如你所見(jiàn),map 上的方法簽名并不沒(méi)有指明是何種對(duì)象在受閉包的控制??纯捶椒w,我們就了解它應(yīng)該是類(lèi)型 Tvalue,但 T 并未在方法簽名中,因此沒(méi)有合適的選項(xiàng)適合 @DelegatesTo。比如我們打算靜態(tài)編譯下列代碼:

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

編譯將失敗,并且提供了以下失敗信息:

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

在這種情況下,可以使用 @DelegatesTo 注釋的 type 成員將 T 引用為類(lèi)型令牌:


class Mapper<T,U> {
    final T value
    Mapper(T value) { this.value = value }
    U map(@DelegatesTo(type="T") Closure<U> producer) {      1??   
        producer.delegate = value
        producer()
    }
}

1?? @DelegatesTo 注釋引用了一個(gè)方法簽名中不存在的基本類(lèi)型。

注意,這里并不局限于基本類(lèi)型令牌。type 成員可以用來(lái)表示復(fù)雜類(lèi)型,比如說(shuō) List<T>Map<T,List<U>>。 它之所以被用作最后手段的原因在于,只有當(dāng)類(lèi)型檢查器發(fā)現(xiàn)使用了 @DelegatesTo 之時(shí),類(lèi)型才被檢查,而不是在注釋方法本身被編譯時(shí)才這樣做。這就意味著類(lèi)型安全性只有在調(diào)用站點(diǎn)時(shí)才能保證。另外,編譯起來(lái)也比較慢(雖然在絕大多數(shù)情況下,這一點(diǎn)并不容易覺(jué)察出來(lái))。

6 編譯自定義器(Compilation customizers)

6.1 簡(jiǎn)介

無(wú)論你使用 groovyc 還是采用 GroovyShell 來(lái)編譯類(lèi),要想執(zhí)行腳本,實(shí)際上都會(huì)使用到編譯器配置compiler configuration)信息。這種配置信息保存了源編碼或類(lèi)路徑這樣的信息,而且還用于執(zhí)行更多的操作,比如默認(rèn)添加導(dǎo)入,顯式使用 AST 轉(zhuǎn)換,或者禁止全局 AST 轉(zhuǎn)換,等等。

編譯自定義器的目標(biāo)在于使這些常見(jiàn)任務(wù)易于實(shí)現(xiàn)。CompilerConfiguration 類(lèi)就是切入點(diǎn)。基本架構(gòu)通常都會(huì)基于下列代碼:

import org.codehaus.groovy.control.CompilerConfiguration
// 創(chuàng)建配置信息  
def config = new CompilerConfiguration()
// 微調(diào)配置信息    
config.addCompilationCustomizers(...)
// 運(yùn)行腳本   
def shell = new GroovyShell(config)
shell.evaluate(script)

編譯自定義器必須擴(kuò)展自 org.codehaus.groovy.control.customizers.CompilationCustomizer 類(lèi)。自定義器適用于:

  • 特定的編譯過(guò)程。
  • 正在編譯的每個(gè)類(lèi)節(jié)點(diǎn)。

當(dāng)然,你可以實(shí)現(xiàn)自己的編譯自定義器,但 Groovy 包含了一些最常見(jiàn)的操作。

6.2. 導(dǎo)入自定義器

使用這種編譯自定義器,代碼可以顯式地添加導(dǎo)入。假如腳本想實(shí)現(xiàn)一種能夠避免用戶(hù)不得不手動(dòng)導(dǎo)入的 DSL,那么這就非常有用了。導(dǎo)入自定義器將使你添加 Groovy 所允許的所有導(dǎo)入形式,包括:

  • 類(lèi)導(dǎo)入,可選別名。
  • 星號(hào)導(dǎo)入。
  • 靜態(tài)導(dǎo)入,可選別名。
  • 靜態(tài)星號(hào)導(dǎo)入。

import org.codehaus.groovy.control.customizers.ImportCustomizer

def icz = new ImportCustomizer()
// 通常的導(dǎo)入
icz.addImports('java.util.concurrent.atomic.AtomicInteger', 'java.util.concurrent.ConcurrentHashMap')
// 別名導(dǎo)入
icz.addImport('CHM', 'java.util.concurrent.ConcurrentHashMap')
// 靜態(tài)導(dǎo)入  
icz.addStaticImport('java.lang.Math', 'PI') // import static java.lang.Math.PI
// 別名靜態(tài)導(dǎo)入
icz.addStaticImport('pi', 'java.lang.Math', 'PI') // import static java.lang.Math.PI as pi
// 星號(hào)導(dǎo)入
icz.addStarImports 'java.util.concurrent' // import java.util.concurrent.*
// 靜態(tài)星號(hào)導(dǎo)入
icz.addStaticStars 'java.lang.Math' // import static java.lang.Math.*

詳細(xì)描述見(jiàn)于 org.codehaus.groovy.control.customizers.ImportCustomizer。

6.3 AST 轉(zhuǎn)換自定義器

AST 轉(zhuǎn)換自定義器可以用來(lái)顯式地應(yīng)用 AST 轉(zhuǎn)換。對(duì)于全局型 AST 轉(zhuǎn)換而言,只要轉(zhuǎn)換存在于類(lèi)路徑中,被編譯的每個(gè)類(lèi)都會(huì)應(yīng)用轉(zhuǎn)換(相應(yīng)的缺點(diǎn)是增加編譯時(shí)間,或者轉(zhuǎn)換了不該轉(zhuǎn)換的)。自定義轉(zhuǎn)換器能實(shí)現(xiàn)選擇應(yīng)用轉(zhuǎn)換,只針對(duì)特定的腳本或類(lèi)應(yīng)用轉(zhuǎn)換。

比如想在腳本中能夠使用 @Log,那么問(wèn)題在于 @Log 一般應(yīng)用于類(lèi)節(jié)點(diǎn)上,而根據(jù)定義,腳本并不需要。但如果實(shí)現(xiàn)得好,腳本也就是類(lèi),只是你不能把這種隱式的類(lèi)節(jié)點(diǎn)用 @Log 來(lái)注釋?zhuān)褂?AST 自定義器,我們可以進(jìn)行一個(gè)全變措施:

import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.util.logging.Log

def acz = new ASTTransformationCustomizer(Log)
config.addCompilationCustomizers(acz)

只需這樣即可!@Log AST 轉(zhuǎn)換被應(yīng)用到編譯單元的每個(gè)類(lèi)節(jié)點(diǎn)上。這意味著它將應(yīng)用到腳本上,以及腳本內(nèi)所定義的類(lèi)上。

如果使用的 AST 轉(zhuǎn)換接收一些參數(shù),也可以在構(gòu)造函數(shù)中使用這些參數(shù):

def acz = new ASTTransformationCustomizer(Log, value: 'LOGGER')
// 使用 'LOGGER' 而非默認(rèn)的 'log'  
config.addCompilationCustomizers(acz)

因?yàn)?AST 轉(zhuǎn)換自定義器用于對(duì)象而不是 AST 節(jié)點(diǎn),所以并不是所有值都被轉(zhuǎn)換為 AST 轉(zhuǎn)換參數(shù)。比如說(shuō),原始類(lèi)型被轉(zhuǎn)換為 ConstantExpressionLOGGER 被轉(zhuǎn)換為 new ConstantExpression('LOGGER')),但如果你的 AST 轉(zhuǎn)換將閉包作為參數(shù),那么必須要給它一個(gè) ClosureExpression,如下例所示:

def configuration = new CompilerConfiguration()
def expression = new AstBuilder().buildFromCode(CompilePhase.CONVERSION) { -> true }.expression[0]
def customizer = new ASTTransformationCustomizer(ConditionalInterrupt, value: expression, thrown: SecurityException)
configuration.addCompilationCustomizers(customizer)
def shell = new GroovyShell(configuration)
shouldFail(SecurityException) {
    shell.evaluate("""
        // 等于添加了 @ConditionalInterrupt(value={true}, thrown: SecurityException)
        class MyClass {
            void doIt() { }
        }
        new MyClass().doIt()
    """)
}

完整的選項(xiàng)列表參見(jiàn):org.codehaus.groovy.control.customizers.ASTTransformationCustomizer。

6.4 安全 AST 自定義器

該自定義器允許 DSL 的開(kāi)發(fā)者限制語(yǔ)言的語(yǔ)法,從而防止用戶(hù)使用一些結(jié)構(gòu)。它只有在這種意義上說(shuō)才是安全的,而且重要的是,它不能代替安全管理器。它存在的唯一理由就是為了限制語(yǔ)言的表現(xiàn)力。自定義器只適用于 AST(抽象語(yǔ)法樹(shù))級(jí)別,而不是在運(yùn)行時(shí)。乍看起來(lái),比較奇怪,但如果把 Groovy 看成是 DSL 的構(gòu)建平臺(tái)的話(huà),就順理成章了。你可能不希望用戶(hù)利用完整的語(yǔ)言。在下例中,只允許使用運(yùn)算操作。該自定義器可以實(shí)現(xiàn):

  • 允許/不允許創(chuàng)建閉包。
  • 允許/不允許導(dǎo)入。
  • 允許/不允許包定義。
  • 允許/不允許方法定義。
  • 限制方法調(diào)用的接收者。
  • 限制用戶(hù)所能使用的 AST 表達(dá)式種類(lèi)。
  • 限制用戶(hù)所能使用的令牌(語(yǔ)法明智)。
  • 限制代碼中常量的類(lèi)型。

安全 AST 自定義器使用白名單(允許的元素列表)或黑名單(不允許的元素列表)策略來(lái)實(shí)現(xiàn)這些功能。對(duì)于每一類(lèi)功能(導(dǎo)入、令牌,等等),都可以選擇究竟使用白名單還是黑名單。還可以混合使用兩種名單來(lái)實(shí)現(xiàn)一些獨(dú)特的功能。一般選擇白名單(不允許選擇還是允許選擇)即可。


import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import static org.codehaus.groovy.syntax.Types.*           1??

def scz = new SecureASTCustomizer()
scz.with {
    closuresAllowed = false // 用戶(hù)不能寫(xiě)閉包
    methodDefinitionAllowed = false // 用戶(hù)不能定義方法
    importsWhitelist = [] // 白名單為空意味著不允許導(dǎo)入
    staticImportsWhitelist = [] // 同樣,對(duì)于靜態(tài)導(dǎo)入也是這樣
    staticStarImportsWhitelist = ['java.lang.Math'] // 只允許 java.lang.Math 
    // 用戶(hù)能找到的令牌列表  
    // org.codehaus.groovy.syntax.Types 中所定義的常量  
    tokensWhitelist = [                  1??
            PLUS,
            MINUS,
            MULTIPLY,
            DIVIDE,
            MOD,
            POWER,
            PLUS_PLUS,
            MINUS_MINUS,
            COMPARE_EQUAL,
            COMPARE_NOT_EQUAL,
            COMPARE_LESS_THAN,
            COMPARE_LESS_THAN_EQUAL,
            COMPARE_GREATER_THAN,
            COMPARE_GREATER_THAN_EQUAL,
    ].asImmutable()
    // 將用戶(hù)所能定義的常量類(lèi)型限制為數(shù)值類(lèi)型
    constantTypesClassesWhiteList = [                2??
            Integer,
            Float,
            Long,
            Double,
            BigDecimal,
            Integer.TYPE,
            Long.TYPE,
            Float.TYPE,
            Double.TYPE
    ].asImmutable()
    // 如果接收者是其中一種類(lèi)型,只允許方法調(diào)用
    // 注意,并不是一個(gè)運(yùn)行時(shí)類(lèi)型! 
    receiversClassesWhiteList = [                   2?? 
            Math,
            Integer,
            Float,
            Double,
            Long,
            BigDecimal
    ].asImmutable()
}

1?? 用于 org.codehaus.groovy.syntax.Types 中的令牌類(lèi)型
2?? 可以使用類(lèi)字面量。

如果安全 AST 自定義器滿(mǎn)足不了你的需求,那么在創(chuàng)建自己的編譯自定義器之前,要考慮一下 AST 自定義器所支持的表達(dá)式和語(yǔ)句檢查器。一般而言,允許在 AST 樹(shù)上,表達(dá)式上(表達(dá)式檢查器)或語(yǔ)句(語(yǔ)句檢查器)添加自定義檢查。為此,必須實(shí)現(xiàn) org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementCheckerorg.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker。

這些接口定義了一個(gè) isAuthorized 方法,它能返回一個(gè)布爾值,能夠接收 StatementExpression 作為參數(shù)。該方法可以在表達(dá)式或語(yǔ)句上實(shí)現(xiàn)復(fù)雜的邏輯,是否允許用戶(hù)去實(shí)現(xiàn)

比如說(shuō),自定義器上沒(méi)有能夠防止用戶(hù)使用某個(gè)屬性表達(dá)式的預(yù)定義配置標(biāo)識(shí),那么使用自定義檢查器,一切就很簡(jiǎn)單:

def scz = new SecureASTCustomizer()
def checker = { expr ->
    !(expr instanceof AttributeExpression)
} as SecureASTCustomizer.ExpressionChecker
scz.addExpressionCheckers(checker)

然后通過(guò)計(jì)算一個(gè)簡(jiǎn)單的腳本就可以確保它的有效性:


new GroovyShell(config).evaluate '''
    class A {
        int val
    }

    def a = new A(val: 123)
    a.@val                      1??
'''

1?? 會(huì)導(dǎo)致編譯失敗。

語(yǔ)句檢查方面可參見(jiàn):
org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker。

表達(dá)式檢查方面參見(jiàn):
org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker。

6.5 源識(shí)別自定義器

該自定義器可以當(dāng)做其他自定義器上的過(guò)濾器。這種情況下的過(guò)濾器是 org.codehaus.groovy.control.SourceUnit。源識(shí)別自定義器將其他自定義器作為一種委托,它只應(yīng)用委托自定義,并且只有在源單位上的謂詞相匹配才進(jìn)行。

SourceUnit 可以讓我們?cè)L問(wèn)多項(xiàng)內(nèi)容,但主要是針對(duì)被編譯的文件(如果編譯的是文件,理當(dāng)如此)??梢愿鶕?jù)文件名稱(chēng)來(lái)實(shí)施操作。范例如下:

import org.codehaus.groovy.control.customizers.SourceAwareCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer

def delegate = new ImportCustomizer()
def sac = new SourceAwareCustomizer(delegate)

然后就可以使用源識(shí)別自定義器上的謂詞了:

// 自定義器只應(yīng)用到名稱(chēng)以 'Bean' 結(jié)尾的文件內(nèi)的類(lèi)上 
sac.baseNameValidator = { baseName ->
    baseName.endsWith 'Bean'
}

// 自定義器只應(yīng)用到擴(kuò)展名為 '.spec' 的文件內(nèi)的類(lèi)上  
sac.extensionValidator = { ext -> ext == 'spec' }

// 源單位驗(yàn)證 
// 只有當(dāng)文件包含至少一個(gè)類(lèi)時(shí)才允許編譯  
sac.sourceUnitValidator = { SourceUnit sourceUnit -> sourceUnit.AST.classes.size() == 1 }

// 類(lèi)驗(yàn)證
// 自定義器只應(yīng)用于結(jié)尾是 `Bean` 的類(lèi)上  
sac.classValidator = { ClassNode cn -> cn.endsWith('Bean') } 

6.6 自定義器構(gòu)建器

如果在 Groovy 代碼中使用編譯自定義器(如上面那些例子所示),則可以采用替代語(yǔ)法來(lái)自定義編譯??梢允褂靡环N構(gòu)建器(org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder)來(lái)簡(jiǎn)化自定義器的創(chuàng)建,使用的是層級(jí) DSL。

import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig         1??

def conf = new CompilerConfiguration()
withConfig(conf) {
    // ...          2??
}

1?? 構(gòu)建器方法的靜態(tài)導(dǎo)入。
2?? 這里放的是相關(guān)配置信息。

上例代碼展示的是構(gòu)建器的使用。靜態(tài)方法 withConfig 獲取一個(gè)跟構(gòu)建器相關(guān)的閉包,自動(dòng)將編譯自定義器注冊(cè)到配置信息中。分發(fā)的每一個(gè)編譯自定義器都可以用這種方式來(lái)配置:

6.6.1 導(dǎo)入自定義器

withConfig(configuration) {
   imports { // imports customizer
      normal 'my.package.MyClass' // a normal import
      alias 'AI', 'java.util.concurrent.atomic.AtomicInteger' // an aliased import
      star 'java.util.concurrent' // star imports
      staticMember 'java.lang.Math', 'PI' // static import
      staticMember 'pi', 'java.lang.Math', 'PI' // aliased static import
   }
}

6.6.2 AST 轉(zhuǎn)換自定義器

withConfig(conf) {
   ast(Log)                     1??
}

withConfig(conf) {
   ast(Log, value: 'LOGGER')        2??
}

1?? 顯式使用 @Log。 2?? 應(yīng)用 @Log ,并使用 logger 的另一個(gè)名字

6.6.3 安全 AST 自定義器

withConfig(conf) {
   secureAst {
       closuresAllowed = false
       methodDefinitionAllowed = false
   }
}

6.6.4 源識(shí)別自定義器


withConfig(configuration){
    source(extension: 'sgroovy') {
        ast(CompileStatic)                 1??
    }
}

withConfig(configuration){
    source(extensions: ['sgroovy','sg']) {
        ast(CompileStatic)                 2??
    }
}

withConfig(configuration) {
    source(extensionValidator: { it.name in ['sgroovy','sg']}) {
        ast(CompileStatic)                  2??
    }
}

withConfig(configuration) {
    source(basename: 'foo') {
        ast(CompileStatic)                  3??
    }
}

withConfig(configuration) {
    source(basenames: ['foo', 'bar']) {
        ast(CompileStatic)                  4?? 
    }
}

withConfig(configuration) {
    source(basenameValidator: { it in ['foo', 'bar'] }) {
        ast(CompileStatic)                  4??
    }
}

withConfig(configuration) {
    source(unitValidator: { unit -> !unit.AST.classes.any { it.name == 'Baz' } }) {
        ast(CompileStatic)                  5?? 
    }
}

1?? 在 .sgroovy 文件上應(yīng)用 AST 注釋 CompileStatic。
2?? 在 .sgroovy 或 .sg 文件上應(yīng)用 AST 注釋 CompileStatic。
3?? 在名稱(chēng)為 foo 的文件上應(yīng)用 AST 注釋 CompileStatic
4?? 在名稱(chēng)為 foo or bar 的文件上應(yīng)用 AST 注釋 CompileStatic。
5?? 在不包含名為 Baz 的類(lèi)的文件上應(yīng)用 AST 注釋 CompileStatic

6.6.5 內(nèi)聯(lián)自定義器

內(nèi)聯(lián)自定義器可以讓你直接編寫(xiě)一個(gè)編譯自定義器,而不必為其創(chuàng)建任何類(lèi):

withConfig(configuration) {
    inline(phase:'CONVERSION') { source, context, classNode ->           1??
        println "visiting $classNode"                                    2??
    }
}

1?? 定義一個(gè)能在 CONVERSION 階段執(zhí)行的內(nèi)聯(lián)自定義器。
2?? 打印正在編輯的類(lèi)節(jié)點(diǎn)名稱(chēng)。

6.6.6 多個(gè)自定義器

當(dāng)然,構(gòu)建器還可以讓你一次構(gòu)建多個(gè)自定義器:

withConfig(configuration) {
   ast(ToString)
   ast(EqualsAndHashCode)
}

6.7 配置腳本標(biāo)記

迄今為止,我們介紹了如何利用 CompilationConfiguration 類(lèi)來(lái)自定義編譯,但這是有一個(gè)前提條件的:內(nèi)嵌 Groovy,并且創(chuàng)建了自己的 CompilerConfiguration 實(shí)例(然后用它來(lái)創(chuàng)建GroovyShell、GroovyScriptEngine,等等)。

如果想把它用在那些利用普通 Groovy 編譯器(也就是說(shuō)利用 groovyc、antgradle)編譯的類(lèi)上,可以使用一個(gè)編譯標(biāo)記 configscript,它以一個(gè) Groovy 配置腳本作為參數(shù)。

該腳本可以讓你在文件編譯前(以名為 configuration 的變量暴露給配置腳本)訪(fǎng)問(wèn) CompilerConfiguration 實(shí)例,因此還可以微調(diào)。

也可以顯式地結(jié)合上面介紹的編譯器配置構(gòu)建器。下例展示了如何在所有的類(lèi)上都默認(rèn)激活靜態(tài)編譯。

6.7.1. 默認(rèn)靜態(tài)編譯

通常,Groovy 中的類(lèi)都是在動(dòng)態(tài)運(yùn)行時(shí)進(jìn)行編譯的??梢园?@CompileStatic 注釋放在任何類(lèi)上來(lái)激活靜態(tài)編譯。一些人可能喜歡默認(rèn)激活這種模式,也就是不用手動(dòng)地去注釋類(lèi)。使用 configscript 就可以。首先需要在 src/conf 上創(chuàng)建一