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 '_\'
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 |
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
方法的直接訪問。
在腳本中直接使用 @BaseScript
注釋也是個不錯的辦法:
import groovy.transform.BaseScript
@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()
上例中,通過 @BaseScript
的注釋,變量類型指定為腳本基類。另一種方法是設(shè)置基類是 @BaseScript
注釋的成員:
@BaseScript(MyBaseClass)
import groovy.transform.BaseScript
setName 'Judith'
greet()
由前面的幾個例子看到,腳本基類屬于一種單獨的抽象方法類型,需要實現(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
在 Groovy 中,數(shù)值類型是等同于其他類型的。因此我們可以通過添加屬性或方法來增強它們的功能。在處理可測量的量時,這樣做尤為方便。關(guān)于如何增強 Groovy 中已有的類的詳細(xì)情況,可參見extension modules或 categories。
使用 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。
@groovy.lang.DelegatesTo
是一個文檔與編譯時注釋,它的主要作用在于:
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
、to
、subject
及 body
各方法。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
、owner
及 thisObject
等值。設(shè)置 owner
和 thisObject
并不十分重要,因為將使用 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)然知道存在一個接收 Closure
的 email
方法,但是它會為閉包內(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'
基于以上這些原因,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!'
}
}
}
@DelegatesTo
支持多種模式,本部分內(nèi)容將予以詳細(xì)介紹。
該模式中唯一強制的參數(shù)是 value,它指明了委托調(diào)用的類。除此之外沒有別的。編譯器還將知道:委托類型將一直是由 @DelegatesTo
所記錄的類型。(注意它可能是個子類,如果是子類的話,對于類型檢查器來說,由該子類所定義的方法是可見的。)
void body(@DelegatesTo(BodySpec) Closure cl) {
// ...
}
在該模式中,必須指定委托類和委托策略。如果閉包沒有以缺省的委托策略(Closure.OWNER_FIRST
)進(jìn)行調(diào)用,就必須使用該模式。
void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
// ...
}
在這種形式中,我們將會告訴編譯器將委托給方法的另一個參數(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ù)。
前例中,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 的一個缺陷。
在一些情況下,可以命令 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
)。
有可能上述所有方式都無法表示你想要委托的類型。比如,可以定義一個 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)該是類型 T
的 value
,但 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ù)情況下,這一點并不容易覺察出來)。
無論你使用 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
類。自定義器適用于:
當(dāng)然,你可以實現(xiàn)自己的編譯自定義器,但 Groovy 包含了一些最常見的操作。
使用這種編譯自定義器,代碼可以顯式地添加導(dǎo)入。假如腳本想實現(xiàn)一種能夠避免用戶不得不手動導(dǎo)入的 DSL,那么這就非常有用了。導(dǎo)入自定義器將使你添加 Groovy 所允許的所有導(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。
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)換為 ConstantExpression
(LOGGER
被轉(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。
該自定義器允許 DSL 的開發(fā)者限制語言的語法,從而防止用戶使用一些結(jié)構(gòu)。它只有在這種意義上說才是安全的,而且重要的是,它不能代替安全管理器。它存在的唯一理由就是為了限制語言的表現(xiàn)力。自定義器只適用于 AST(抽象語法樹)級別,而不是在運行時。乍看起來,比較奇怪,但如果把 Groovy 看成是 DSL 的構(gòu)建平臺的話,就順理成章了。你可能不希望用戶利用完整的語言。在下例中,只允許使用運算操作。該自定義器可以實現(xiàn):
安全 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.StatementChecker
或 org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
。
這些接口定義了一個 isAuthorized
方法,它能返回一個布爾值,能夠接收 Statement
或 Expression
作為參數(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。
該自定義器可以當(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') }
如果在 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ā)的每一個編譯自定義器都可以用這種方式來配置:
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
}
}
withConfig(conf) {
ast(Log) 1??
}
withConfig(conf) {
ast(Log, value: 'LOGGER') 2??
}
1?? 顯式使用 @Log
。
2?? 應(yīng)用 @Log
,并使用 logger 的另一個名字
withConfig(conf) {
secureAst {
closuresAllowed = false
methodDefinitionAllowed = false
}
}
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
。
內(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é)點名稱。
當(dāng)然,構(gòu)建器還可以讓你一次構(gòu)建多個自定義器:
withConfig(configuration) {
ast(ToString)
ast(EqualsAndHashCode)
}
迄今為止,我們介紹了如何利用 CompilationConfiguration
類來自定義編譯,但這是有一個前提條件的:內(nèi)嵌 Groovy,并且創(chuàng)建了自己的 CompilerConfiguration
實例(然后用它來創(chuàng)建GroovyShell
、GroovyScriptEngine
,等等)。
如果想把它用在那些利用普通 Groovy 編譯器(也就是說利用 groovyc
、ant
或 gradle
)編譯的類上,可以使用一個編譯標(biāo)記 configscript
,它以一個 Groovy 配置腳本作為參數(shù)。
該腳本可以讓你在文件編譯前(以名為 configuration
的變量暴露給配置腳本)訪問 CompilerConfiguration
實例,因此還可以微調(diào)。
也可以顯式地結(jié)合上面介紹的編譯器配置構(gòu)建器。下例展示了如何在所有的類上都默認(rèn)激活靜態(tài)編譯。
通常,Groovy 中的類都是在動態(tài)運行時進(jìn)行編譯的。可以把 @CompileStatic
注釋放在任何類上來激活靜態(tài)編譯。一些人可能喜歡默認(rèn)激活這種模式,也就是不用手動地去注釋類。使用 configscript
就可以。首先需要在 src/conf
上創(chuàng)建一