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 '_\'
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 |
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)。
在腳本中直接使用 @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()
由前面的幾個(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
在 Groovy 中,數(shù)值類(lèi)型是等同于其他類(lèi)型的。因此我們可以通過(guò)添加屬性或方法來(lái)增強(qiáng)它們的功能。在處理可測(cè)量的量時(shí),這樣做尤為方便。關(guān)于如何增強(qiáng) Groovy 中已有的類(lèi)的詳細(xì)情況,可參見(jiàn)extension modules或 categories。
使用 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。
@groovy.lang.DelegatesTo
是一個(gè)文檔與編譯時(shí)注釋?zhuān)闹饕饔迷谟冢?
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
、to
、subject
及 body
各方法。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è)置 delegate
、owner
及 thisObject
等值。設(shè)置 owner
和 thisObject
并不十分重要,因?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è)接收 Closure
的 email
方法,但是它會(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'
基于以上這些原因,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!'
}
}
}
@DelegatesTo
支持多種模式,本部分內(nèi)容將予以詳細(xì)介紹。
該模式中唯一強(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) {
// ...
}
在該模式中,必須指定委托類(lèi)和委托策略。如果閉包沒(méi)有以缺省的委托策略(Closure.OWNER_FIRST
)進(jìn)行調(diào)用,就必須使用該模式。
void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
// ...
}
在這種形式中,我們將會(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ù)。
前例中,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è)缺陷。
在一些情況下,可以命令 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
)。
有可能上述所有方式都無(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)型 T
的 value
,但 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))。
無(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)。自定義器適用于:
當(dāng)然,你可以實(shí)現(xiàn)自己的編譯自定義器,但 Groovy 包含了一些最常見(jiàn)的操作。
使用這種編譯自定義器,代碼可以顯式地添加導(dǎo)入。假如腳本想實(shí)現(xiàn)一種能夠避免用戶(hù)不得不手動(dòng)導(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
// 星號(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。
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)換為 ConstantExpression
(LOGGER
被轉(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。
該自定義器允許 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):
安全 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.StatementChecker
或 org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
。
這些接口定義了一個(gè) isAuthorized
方法,它能返回一個(gè)布爾值,能夠接收 Statement
或 Expression
作為參數(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。
該自定義器可以當(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') }
如果在 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)配置:
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 的另一個(gè)名字
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?? 在名稱(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
。
內(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)。
當(dāng)然,構(gòu)建器還可以讓你一次構(gòu)建多個(gè)自定義器:
withConfig(configuration) {
ast(ToString)
ast(EqualsAndHashCode)
}
迄今為止,我們介紹了如何利用 CompilationConfiguration
類(lèi)來(lái)自定義編譯,但這是有一個(gè)前提條件的:內(nèi)嵌 Groovy,并且創(chuàng)建了自己的 CompilerConfiguration
實(shí)例(然后用它來(lái)創(chuàng)建GroovyShell
、GroovyScriptEngine
,等等)。
如果想把它用在那些利用普通 Groovy 編譯器(也就是說(shuō)利用 groovyc
、ant
或 gradle
)編譯的類(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)編譯。
通常,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)建一