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

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

1 命令鏈

Groovy 可以使你省略頂級語句方法調(diào)用中參數(shù)外面的括號?!懊铈湣惫δ軇t將這種特性繼續(xù)擴展,它可以將不需要括號的方法調(diào)用串接成鏈,既不需要參數(shù)周圍的括號,鏈接的調(diào)用之間也不需要點號。舉例來說,a b c d 實際上就等同于 a(b).c(d)。它適用于多個參數(shù)、閉包參數(shù),甚至命名參數(shù)。而且,這樣的命令鏈也可以出現(xiàn)在賦值的右方。讓我們來看看應(yīng)用這一新的語法格式的范例:

// 等同于: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ù)的鏈中也可以使用方法,但在這種情況下需要用括號。

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

如果命令鏈包含奇數(shù)個元素,鏈會由方法和參數(shù)組成,最終由一個最終屬性訪問:

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

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

上面的范例展示了使用基于 DSL 的命令鏈,但是不知道如何創(chuàng)建一個。你可以使用很多策略,但是為了展示如何創(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

第二個范例要考慮如何編寫 DSL 來簡化一個現(xiàn)存的 API?;蛟S你需要把這些代碼展示給顧客、業(yè)務(wù)分析師或測試員,有可能這些人并不是技藝非常精湛的 Java 開發(fā)者。我們將使用 Splitter,一個Google 的 Guava libraries 項目,因為它已經(jīng)是一個良好的 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()

對于 Java 開發(fā)者而言,這段代碼很容易明白,但如果面對的是目標(biāo)顧客,或者要寫很多這樣的語句,那就可能顯得有些啰嗦了。再次提醒一下,DSL 的編寫手段多種多樣。利用映射和閉包來將它簡化一下,首先編寫一個輔助函數(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 的多種操作符都可以被映射到對象的正則方法調(diào)用上。

這允許你提供自己的 Java 或 Groovy 對象,以便利用操作符重載這一優(yōu)點。下面這張表展示了 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. 腳本基類

3.1 Script 類

Groovy 腳本經(jīng)常被編譯為類。比如下面這個簡單的腳本:

println 'Hello from Groovy'

會被編譯擴展自 groovy.lang.Script 抽象類的類。該類只包含一個抽象方法:run。當(dāng)腳本編譯時,語句體就會成為 run 方法,腳本中的其他方法都位于實現(xiàn)類中。Script 類為通過 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)用類間共享數(shù)據(jù)的綁定對象。
2?? 與該綁定對象聯(lián)合使用的 GroovyShell。
3?? 輸入變量從位于綁定對象內(nèi)部的調(diào)用類進(jìn)行設(shè)置。
4?? 然后計算腳本。
5?? z 變量導(dǎo)出到綁定對象中。

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

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

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

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)建一個自定義的編譯器配置。
2?? 將腳本基類設(shè)為我們自定義的腳本基類。
3?? 然后創(chuàng)建一個使用該配置的 GroovyShell。 4?? 腳本然后擴展該腳本基類,提供對 name 屬性及 greet 方法的直接訪問。

3.2 @BaseScript 注釋

在腳本中直接使用 @BaseScript 注釋也是個不錯的辦法:

import groovy.transform.BaseScript

@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()

上例中,通過 @BaseScript 的注釋,變量類型指定為腳本基類。另一種方法是設(shè)置基類是 @BaseScript 注釋的成員:

@BaseScript(MyBaseClass)
import groovy.transform.BaseScript

setName 'Judith'
greet()

3.3 替代的抽象方法

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

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

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

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

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

你就會看到腳本被執(zhí)行,但計算結(jié)果是由基類的 run 方法所返回的 1。如果使用 parse 代替 evaluate,你就會更清楚,因為你會在同一腳本實例上執(zhí)行多次 run 方法。

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

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

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

使用 TimeCategory 可展示 Groovy 中的一個范例:

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 類上。
2?? 同樣,months 方法也將返回一個用于計算的 groovy.time.DatumDependentDuration。

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

5. @DelegatesTo

5.1. 編譯時解釋委托策略

@groovy.lang.DelegatesTo 是一個文檔與編譯時注釋,它的主要作用在于:

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

Groovy 是構(gòu)建 DSL 的一種選擇平臺。使用閉包可以非常輕松地創(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)建者策略可實現(xiàn),利用一個參數(shù)為閉包的名為 email 的方法,它會將隨后的調(diào)用委托給一個對象,該對象實現(xiàn)了 from、tosubjectbody 各方法。body 方法使用閉包做參數(shù),使用的是構(gòu)建者策略。

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

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

EmailSpec 類實現(xiàn)了 from、to 等方法,通過調(diào)用 rehydrate,創(chuàng)建了一個閉包副本,用于為該副本設(shè)置 delegate、ownerthisObject 等值。設(shè)置 ownerthisObject 并不十分重要,因為將使用 DELEGATE_ONLY 策略,解決方法調(diào)用只針對的是閉包委托。

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 類自身的 body 方法將接受一個復(fù)制并執(zhí)行的閉包,這就是 Groovy 構(gòu)建者模式的原理。

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

如果用戶調(diào)用了閉包內(nèi)的一個方法,該方法沒有被 EmailSpec 類所定義,IDE 應(yīng)該至少能提供一個警告(因為這非常有可能會在運行時造成崩潰)。

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

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

類型檢查器當(dāng)然知道存在一個接收 Closureemail 方法,但是它會為閉包內(nèi)的每個方法都進(jìn)行解釋,比如說 from,它不是一個定義在類中的方法,實際上它定義在 EmailSpec 類中,但在運行時,沒有任何線索能讓檢查器知道它的閉包委托類型是 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!'
        }
    }
}

所以在編譯時會失敗,錯誤信息如下:

[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 引入了一個新的注釋:@DelegatesTo。該注釋目的在于解決文檔問題,讓 IDE 了解閉包體內(nèi)的期望方法。同時,它還能夠給編譯器提供一些提示,告知編譯器閉包體內(nèi)的方法調(diào)用的可能接收者是誰,從而解決類型檢查問題。

具體方法是注釋 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)的方法何時被調(diào)用,閉包委托被設(shè)置為 email 類型對象。但這里仍遺漏了一個問題:默認(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 和類型檢查器(如果使用 @TypeChecked)都能知道委托和委托策略了?,F(xiàn)在,IDE 不僅可以進(jìn)行智能補足,而且還能消除編譯時出現(xiàn)的錯誤,而這種錯誤的產(chǎ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. 簡單委托

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

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

5.3.2. 委托策略

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

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

5.3.3. 委托給參數(shù)

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

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

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

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

每個方法調(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,但這一次,沒有指定任何類,而利用 @DelegatesTo.Target 注釋了另一個參數(shù)。委托類型在編譯時進(jìn)行指定??赡苡腥藭J(rèn)為使用參數(shù)類型,比如在該例中是 Object,但這是錯的。代碼如下:

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

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

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

5.3.4 多個閉包

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

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

利用 @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 $qibc6mg!" } }

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

但更重要的是,如果有多個閉包和多個參數(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ù)名呢?因為相關(guān)信息(參數(shù)名)不一定能獲取到(只供調(diào)試的信息),所以這是 JVM 的一個缺陷。

5.3.5. 委托給基本類型

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

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' }

要想讓類型檢查器和 IDE 了解 configure 方法在列表的每個元素上調(diào)用閉包,你需要換一種方式來使用 @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 獲取一個可選的 genericTypeIndex 參數(shù),該參數(shù)指出被用作委托類型的基本類型的具體索引。它必須要與 @DelegatesTo.Target 聯(lián)合使用,并且起始索引要為 0。在上例中,委托類型會根據(jù) List<T> 來判定,因為在索引 0 處的基本類型是 T,并推斷是 Realm,所以類型檢查器也會推斷委托類型屬于 Realm 類型。

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

5.3.6. 委托給任意類型

有可能上述所有方式都無法表示你想要委托的類型。比如,可以定義一個 Mapper 類,它帶有一個對象參數(shù),并且定義了一個能夠返回其他類型對象的 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 類接受兩個通用類型參數(shù):源類型與目標(biāo)類型。
2?? 源對象保存在一個 final 類型的對象中。
3?? map 方法請求將源對象轉(zhuǎn)換為目標(biāo)對象。

如你所見,map 上的方法簽名并不沒有指明是何種對象在受閉包的控制??纯捶椒w,我們就了解它應(yīng)該是類型 Tvalue,但 T 并未在方法簽名中,因此沒有合適的選項適合 @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 引用為類型令牌:


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 注釋引用了一個方法簽名中不存在的基本類型。

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

6 編譯自定義器(Compilation customizers)

6.1 簡介

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

編譯自定義器的目標(biāo)在于使這些常見任務(wù)易于實現(xiàn)。CompilerConfiguration 類就是切入點?;炯軜?gòu)通常都會基于下列代碼:

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

編譯自定義器必須擴展自 org.codehaus.groovy.control.customizers.CompilationCustomizer 類。自定義器適用于:

  • 特定的編譯過程。
  • 正在編譯的每個類節(jié)點。

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

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

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

  • 類導(dǎo)入,可選別名。
  • 星號導(dǎo)入。
  • 靜態(tài)導(dǎo)入,可選別名。
  • 靜態(tài)星號導(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
// 星號導(dǎo)入
icz.addStarImports 'java.util.concurrent' // import java.util.concurrent.*
// 靜態(tài)星號導(dǎo)入
icz.addStaticStars 'java.lang.Math' // import static java.lang.Math.*

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

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

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

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

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)用到編譯單元的每個類節(jié)點上。這意味著它將應(yīng)用到腳本上,以及腳本內(nèi)所定義的類上。

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

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

因為 AST 轉(zhuǎn)換自定義器用于對象而不是 AST 節(jié)點,所以并不是所有值都被轉(zhuǎn)換為 AST 轉(zhuǎn)換參數(shù)。比如說,原始類型被轉(zhuǎn)換為 ConstantExpressionLOGGER 被轉(zhuǎn)換為 new ConstantExpression('LOGGER')),但如果你的 AST 轉(zhuǎn)換將閉包作為參數(shù),那么必須要給它一個 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()
    """)
}

完整的選項列表參見:org.codehaus.groovy.control.customizers.ASTTransformationCustomizer。

6.4 安全 AST 自定義器

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

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

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


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

def scz = new SecureASTCustomizer()
scz.with {
    closuresAllowed = false // 用戶不能寫閉包
    methodDefinitionAllowed = false // 用戶不能定義方法
    importsWhitelist = [] // 白名單為空意味著不允許導(dǎo)入
    staticImportsWhitelist = [] // 同樣,對于靜態(tài)導(dǎo)入也是這樣
    staticStarImportsWhitelist = ['java.lang.Math'] // 只允許 java.lang.Math 
    // 用戶能找到的令牌列表  
    // 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()
    // 將用戶所能定義的常量類型限制為數(shù)值類型
    constantTypesClassesWhiteList = [                2??
            Integer,
            Float,
            Long,
            Double,
            BigDecimal,
            Integer.TYPE,
            Long.TYPE,
            Float.TYPE,
            Double.TYPE
    ].asImmutable()
    // 如果接收者是其中一種類型,只允許方法調(diào)用
    // 注意,并不是一個運行時類型! 
    receiversClassesWhiteList = [                   2?? 
            Math,
            Integer,
            Float,
            Double,
            Long,
            BigDecimal
    ].asImmutable()
}

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

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

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

比如說,自定義器上沒有能夠防止用戶使用某個屬性表達(dá)式的預(yù)定義配置標(biāo)識,那么使用自定義檢查器,一切就很簡單:

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

然后通過計算一個簡單的腳本就可以確保它的有效性:


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

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

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

語句檢查方面可參見:
org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker

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

6.5 源識別自定義器

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

SourceUnit 可以讓我們訪問多項內(nèi)容,但主要是針對被編譯的文件(如果編譯的是文件,理當(dāng)如此)??梢愿鶕?jù)文件名稱來實施操作。范例如下:

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

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

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

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

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

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

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

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

如果在 Groovy 代碼中使用編譯自定義器(如上面那些例子所示),則可以采用替代語法來自定義編譯??梢允褂靡环N構(gòu)建器(org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder)來簡化自定義器的創(chuàng)建,使用的是層級 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òu)建器相關(guān)的閉包,自動將編譯自定義器注冊到配置信息中。分發(fā)的每一個編譯自定義器都可以用這種方式來配置:

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 的另一個名字

6.6.3 安全 AST 自定義器

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

6.6.4 源識別自定義器


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?? 在名稱為 foo 的文件上應(yīng)用 AST 注釋 CompileStatic
4?? 在名稱為 foo or bar 的文件上應(yīng)用 AST 注釋 CompileStatic
5?? 在不包含名為 Baz 的類的文件上應(yīng)用 AST 注釋 CompileStatic。

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

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

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

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

6.6.6 多個自定義器

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

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

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

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

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

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

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

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

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