# 實戰 Groovy: SwingBuilder 和 Twitter API,第 1 部分
_構建基于 Swing 的 GUI 從未如此簡便_
在這一期 [_實戰 Groovy_](http://www.ibm.com/developerworks/cn/java/j-pg/) 中,Scott Davis 要討論一個令大多數服務器端 Java? 開發人員畏懼的主題:Swing。Groovy 的 `SwingBuilder` 可以讓這個強大但復雜的 GUI 框架使用起來簡單一些。
我最近會見了 Ted Neward,他是 IBM developerWorks 文章系列 [_面向 Java 開發人員的 Scala 指南_](http://www.ibm.com/developerworks/cn/java/j-scala/) 的作者(見 [參考資料](#resources))。我們討論了他在這個系列中構建的一個有意思的 Twitter 庫,Scitter (Scala + Twitter)。Scitter 的重點在于 Scala 的 Web 服務和 XML 解析功能,Ted 承認他不太關心為這個 API 提供前端。當然,這啟發我考慮用 Groovy 編寫一個 Twitter GUI 會怎么樣?_Gwitter_ (Groovy + Twitter) 是個不錯的名字吧?
在本文中我不打算討論 Scala 和 Groovy 的集成,盡管在這兩種語言之間確實有許多協作的可能性。相反,我要討論 Java 領域中常常被 Java 開發人員忽視的一個主題:Swing。但是,在此之前,我先談談 Groovy 的 `XmlSlurper` 如何簡化 Twitter 的 Atom feed。
## Twitter Search API
看一下 Twitter Search API 的在線文檔(見 [參考資料](#resources))。文檔表明可以通過發出簡單的 HTTP GET 請求搜索 Twitter。查詢通過查詢字符串中的 `q` 參數傳遞,結果以 Atom(一種 XML 聯合格式)或 JavaScript Object Notation (JSON) 的形式返回。因此,要想以 Atom 的形式得到所有提到 _thirstyhead_ 的條目,需要發出下面這樣的 HTTP GET 請求:`http://search.twitter.com/search.atom?q=thirstyhead`。
如清單 1 所示,返回的結果是嵌套在 `<feed>` 元素中的一系列 `<entry>` 元素:
##### 清單 1\. Twitter 搜索 Atom 結果
```
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
<entry>
<title>thirstyhead: New series from Andrew Glover: Java Development 2.0
http://bit.ly/bJX5i</title>
<content type="html">thirstyhead: New series from Andrew Glover: Java
Development 2.0 http://bit.ly/bJX5i</content>
<id>tag:twitter.com,2007:
http://twitter.com/thirstyhead/statuses/3419507135</id>
<published>2009-08-20T02:54:54+00:00</published>
<updated>2009-08-20T02:54:54+00:00</updated>
<link type="text/html" rel="alternate"
href="http://twitter.com/thirstyhead/statuses/3419507135"/>
<link type="image/jpeg" rel="image"
href="http://s3.amazonaws.com/twitter_production/profile_images/
73550313/flame_normal.jpg"/>
<author>
<name>ThirstyHead.com</name>
<uri>http://www.thirstyhead.com</uri>
</author>
</entry>
<entry>...</entry>
<entry>...</entry>
<!-- snip -->
</feed>
```
在 “[實戰 Groovy:構建和解析 XML](http://www.ibm.com/developerworks/cn/java/j-pg05199/)” 中,可以看到很容易使用 Groovy 的 `XmlSlurper` 處理 XML 結果。既然了解了這些結果的形式,就來創建一個名為 searchCli.groovy 的文件,見清單 2:
##### 清單 2\. 解析 Atom 結果的 Groovy 腳本
```
if(args){
def username = args[0]
def addr = "http://search.twitter.com/search.atom?q=${username}"
def feed = new XmlSlurper().parse(addr)
feed.entry.each{
println it.author.name
println it.published
println it.title
println "-"*20
}
}else{
println "USAGE: groovy searchCli <query>"
}
```
在命令行上輸入 `groovy searchCli thirstyhead`,就會顯示簡潔的 Atom 結果,見清單 3:
##### 清單 3\. 運行 searchCli.groovy 腳本
```
$ groovy searchCli thirstyhead
thirstyhead (ThirstyHead.com)
2009-08-20T02:54:54Z
New series from Andrew Glover:
Java Development 2.0 http://bit.ly/bJX5i
--------------------
kung_foo (kung_foo)
2009-08-18T12:33:32Z
ThirstyHead interviews Venkat Subramaniam:
http://blip.tv/file/2484840 "Groovy and Scala are good friends..."
(via @mittie). very good.
//snip
```
* * *
## 創建最初的 Gwitter 類
Groovy 腳本很適合編寫非正式的實用程序和證實概念,但是編寫 Groovy 類也不太困難。另外,可以編譯 Groovy 類并從 Java 代碼調用它們。
例如,可以編寫清單 4 所示的 Tweet.groovy:
##### 清單 4\. Tweet.groovy
```
class Tweet{
String content
String published
String author
String toString(){
return "${author}: ${content}"
}
}
```
這是一個 Plain Old Groovy Object (POGO),是非常復雜的 Plain Old Java Object (POJO) 的替代品。
現在,把 [清單 2](#listing2) 中的搜索腳本轉換為 Search.groovy,見清單 5:
##### 清單 5\. Search.groovy
```
class Search{
static final String addr = "http://search.twitter.com/search.atom?q="
static Object[] byKeyword(String query){
def results = []
def feed = new XmlSlurper().parse(addr + query)
feed.entry.each{entry->
def tweet = new Tweet()
tweet.author = entry.author.name
tweet.published = entry.published
tweet.content = entry.title
results << tweet
}
return results as Object[]
}
}
```
通常情況下,我會讓結果保持 `java.util.ArrayList` 的形式。但是,本文后面使用的 `javax.swing.JList` 需要一個 `Object[]`,所以這里提前做一些準備。
注意,我在 Search.groovy 中去掉了 `main()` 方法。現在如何與這個類交互呢?當然可以通過單元測試!創建 SearchTest.groovy,見清單 6:
##### 清單 6\. SearchTest.groovy
```
class SearchTest extends GroovyTestCase{
void testSearchByKeyword(){
def results = Search.byKeyword("thirstyhead")
results.each{
assertTrue it.content.toLowerCase().contains("thirstyhead") ||
it.author.toLowerCase().contains("thirstyhead")
}
}
}
```
如果在命令提示上輸入 `groovy SearchTest`,然后看到 `OK (1 test)`(見清單 7),就說明已經成功地把搜索腳本轉換為可重用的類了:
##### 清單 7\. 成功測試的運行結果
```
$ groovy SearchTest
.
Time: 4.64
OK (1 test)
```
現在底層基礎結構已經就位了,下一步是開始為它提供漂亮的前端。
* * *
## `SwingBuilder` 簡介
Swing 是一個極其強大的 GUI 工具集。但糟糕的是,有時候其復雜性會影響開發人員揮發它的能力。如果您剛接觸 Swing,會覺得像是在學習開波音 747,而您實際上只需要開單引擎的 Cessna 或滑翔機。
Groovy 的 `SwingBuilder` 并不能降低各種任務內在的復雜性,比如選擇適當的 `LayoutManager` 或處理線程問題。它降低的是語法復雜性。Groovy 的命名參數/變量參數構造器非常適合需要實例化的各種 `JComponent`,然后馬上可以為它們配置一系列設置器。(關于 `SwingBuilder` 的更多信息,請參見 [參考資料](#resources))。
但是,同樣有價值的是 Groovy 對閉包的使用。對于 Swing,我長期關注的問題是自然的層次結構似乎在實現細節中消失了。在 Java 代碼中,會得到一組相互脫節的組件,看不出哪個組件屬于哪個組件。可以以任意次序聲明 `JFrame`、`JPanel` 和 `JLabel`。在代碼中,它們看起來是平等的;但是,實際上 `JFrame` 包含 `JPanel`,`JPanel` 進而包含 `JLabel`。清單 8 給出一個示例:
##### 清單 8\. HelloJavaSwing.java
```
import javax.swing.*;
public class HelloJavaSwing {
public static void main(String[] args) {
JPanel panel = new JPanel();
JLabel label = new JLabel("Hello Java Swing");
JFrame frame = new JFrame("Hello Java Swing");
panel.add(label);
frame.add(panel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(200,300);
frame.setVisible(true);
}
}
```
編譯這段代碼 (`javac HelloJavaSwing.java`) 并運行它 (`java HelloJava`),應該會顯示圖 1 所示的應用程序:
##### 圖 1\. `HelloJavaSwing`

清單 9 給出用 Groovy 編寫的同一個應用程序。可以看到 `SwingBuilder` 使用了閉包,這讓我們可以清晰地看出擁有關系鏈。
##### 清單 9\. HelloGroovySwing.groovy
```
import groovy.swing.SwingBuilder
import javax.swing.*
def swingBuilder = new SwingBuilder()
swingBuilder.frame(title:"Hello Groovy Swing",
defaultCloseOperation:JFrame.EXIT_ON_CLOSE,
size:[200,300],
show:true) {
panel(){
label("Hello Groovy Swing")
}
}
```
輸入 `groovy HelloGroovySwing` 會看到圖 2 所示的應用程序:
##### 圖 2\. `HelloGroovySwing`

注意,在 [清單 9](#listing9) 中,所有組件名去掉了開頭的 `J`,方法名中也去掉了多余的 `get` 和 `set`。接下來,注意 `frame` 的命名參數構造器。在幕后,Groovy 調用無參數構造器,然后調用設置器方法,這與前面的 Java 示例沒有區別。但是,設置器方法都集中在構造器中,代碼更簡潔了,去掉 `set` 前綴和末尾的圓括號也大大減少了視覺干擾。
如果您不了解 Swing,這段代碼看起來可能仍然比較復雜。但是,如果您具備哪怕最粗淺的 Swing 經驗,就可以看出它具有 Swing 的特征:干凈、清晰和高效。
正如在前一節中所做的,通過腳本了解概念,然后把腳本轉換為類。創建文件 Gwitter.groovy,見清單 10。這是 Groovy + Twitter 客戶機 UI 的起點。
##### 清單 10\. Gwitter UI 的骨架
```
import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
class Gwitter{
static void main(String[] args){
def gwitter = new Gwitter()
gwitter.show()
}
void show(){
def swingBuilder = new SwingBuilder()
swingBuilder.frame(title:"Gwitter",
defaultCloseOperation:JFrame.EXIT_ON_CLOSE,
size:[400,500],
show:true) {
}
}
}
```
輸入 `groovy Gwitter`,確認會出現空的框架。如果一切正常,下一步是在應用程序中添加一個簡單的菜單。
* * *
## 添加菜單欄
在 Swing 中創建菜單提供另一個具有自然層次結構的組件示例。創建一個 `JMenuBar`,它包含一個或多個 `JMenu`,`JMenu` 進而包含一個或多個 `JMenuItem`。
為了創建包含 `Exit` 菜單項的 `File` 菜單,在 Gwitter.groovy 中添加清單 11 中的代碼:
##### 清單 11\. 在 Gwitter 中添加 `File` 菜單
```
import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
class Gwitter{
static void main(String[] args){
def gwitter = new Gwitter()
gwitter.show()
}
void show(){
def swingBuilder = new SwingBuilder()
def customMenuBar = {
swingBuilder.menuBar{
menu(text: "File", mnemonic: 'F') {
menuItem(text: "Exit", mnemonic: 'X', actionPerformed: { dispose() })
}
}
}
swingBuilder.frame(title:"Gwitter",
defaultCloseOperation:JFrame.EXIT_ON_CLOSE,
size:[400,500],
show:true) {
customMenuBar()
}
}
}
```
請注意 `customMenuBar` 閉包的嵌套層次結構。為了便于閱讀,這里添加了換行和縮進,但是同樣很容易在同一行中定義它。定義這個閉包之后,在 `frame` 閉包中調用它。再次輸入 `groovy Gwitter`,確認會出現 `File` 菜單,見圖 4。選擇 **File > Exit**,關閉這個應用程序。
##### 圖 4\. Gwitter 的 File 菜單

再看看 [清單 11](#listing11)。注意,`actionPerformed` 處理函數定義為閉包,而不是匿名類。與相應的 Java 代碼相比,這樣的代碼更干凈、更容易閱讀。
現在,添加一些表單元素以執行搜索。
* * *
## 添加搜索面板
經驗豐富的 Swing 開發人員善于用單獨的 `JPanel` 組裝出最終的應用程序。這些容器組件可以方便地把相似、相關的組件分組在一起。
例如,Gwitter 需要一個 `JTextField`(讓用戶能夠輸入搜索條件)和一個 `JButton` (用于提交請求)。把這兩個組件分組在一個 `searchPanel` 閉包中是有意義的,見清單 12:
##### 清單 12\. 添加搜索面板
```
import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
class Gwitter{
def searchField
static void main(String[] args){
def gwitter = new Gwitter()
gwitter.show()
}
void show(){
def swingBuilder = new SwingBuilder()
def customMenuBar = {
swingBuilder.menuBar{
menu(text: "File", mnemonic: 'F') {
menuItem(text: "Exit", mnemonic: 'X', actionPerformed: {dispose() })
}
}
}
def searchPanel = {
swingBuilder.panel(constraints: BorderLayout.NORTH){
searchField = textField(columns:15)
button(text:"Search", actionPerformed:{ /* TODO */ } )
}
}
swingBuilder.frame(title:"Gwitter",
defaultCloseOperation:JFrame.EXIT_ON_CLOSE,
size:[400,500],
show:true) {
customMenuBar()
searchPanel()
}
}
}
```
開始處理面板之后,就要選擇適當的 `LayoutManger`。在默認情況下,`JPanel` 使用 `FlowLayout`。這意味著 `textField` 和 `button` 挨著水平排列。
`JFrame` 的 `contentPane` 不太一樣 — 它在默認情況下使用 `BorderLayout`。這意味著在框架中添加 `searchPanel` 時需要指定它應該出現在哪個區域:`NORTH`、`SOUTH`、`EAST`、`WEST` 或 `CENTER`。(如果您的地理知識實在糟糕,也可以使用 `PAGE_START`、`PAGE_END`、`LINE_START`、`LINE_END` 和 `CENTER`)。關于 Swing 中可用的各種 `LayoutManager` 的更多信息,請參見 [參考資料](#resources)。
注意,`searchField` 變量是在類級聲明的。因此,按鈕等其他組件也可以訪問它。其他組件都是匿名的。快速瀏覽一下類屬性,就會看出某些組件比較重要。
您可能已經注意到按鈕的 `actionPerformed` 監聽器目前沒有做任何事情。現在實際上還不需要它做什么。在實現它之前,需要在應用程序中添加另一個面板:用來顯示搜索結果的面板。
* * *
## 添加結果面板
如清單 13 所示,像對待 `searchPanel` 那樣,在嵌套的閉包中定義 `resultsPanel`。但是,這一次在這個面板中嵌套另一個容器:`JScrollPane`。這個組件可以根據需要顯示和隱藏水平和垂直滾動條。`Search.byKeyword()` 方法調用的結果顯示在名為 `resultsList` 的 `JList` 中。(`JList.setListData()` 方法接受一個 `Object[]`— 這就是 `Search.byKeyword()` 方法返回的結果)。
##### 清單 13\. 添加 `resultsPanel`
```
import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
class Gwitter{
def searchField
def resultsList
static void main(String[] args){
def gwitter = new Gwitter()
gwitter.show()
}
void show(){
def swingBuilder = new SwingBuilder()
def customMenuBar = {
swingBuilder.menuBar{
menu(text: "File", mnemonic: 'F') {
menuItem(text: "Exit", mnemonic: 'X', actionPerformed: {dispose() })
}
}
}
def searchPanel = {
swingBuilder.panel(constraints: BorderLayout.NORTH){
searchField = textField(columns:15)
button(text:"Search", actionPerformed:{
resultsList.listData = Search.byKeyword(searchField.text) } )
}
}
def resultsPanel = {
swingBuilder.scrollPane(constraints: BorderLayout.CENTER){
resultsList = list()
}
}
swingBuilder.frame(title:"Gwitter",
defaultCloseOperation:JFrame.EXIT_ON_CLOSE,
size:[400,500],
show:true) {
customMenuBar()
searchPanel()
resultsPanel()
}
}
}
```
注意,與 `searchField` 一樣,`resultsList` 變量是在類級定義的。`searchPanel` 中按鈕的 `actionPerformed` 處理函數使用這兩個變量。
添加 `resultsPanel` 之后,Gwitter 現在有實際功能了。在命令提示上輸入 `groovy Gwitter`,檢查它是否工作正常。搜索 _thirstyhead_ 應該會產生圖 5 所示的結果:
##### 圖 5\. 搜索結果

現在可以宣布成功了,但是我想先解決兩個問題。第一個問題是搜索按鈕的 `actionPerformed` 處理函數可能會引起線程問題。另一個問題是這個應用程序太一般了。下面兩節解決這些問題。
* * *
## 事件分派線程
Swing 的缺點在于,它期望圖形設計師能夠應付多線程問題,而這是應該由軟件工程師處理的,或者期望軟件工程師理解圖形設計和易用性問題。
我不可能在短短幾段文字中討論 Swing 應用程序中的線程問題這么復雜的主題。只需指出基本的 Swing 應用程序本質上是單線程的。所有活動都在事件分派線程 (EDT) 上進行。當用戶抱怨 Swing 應用程序反應遲緩或完全沒有反應時,往往是因為某個開發新手在 EDT 上執行長時間的計算密集型的數據庫查詢或 Web 服務調用 — 這個線程也負責處理屏幕刷新、菜單單擊等。我們無意中在搜索按鈕的 `actionPerformed` 處理函數上犯了同樣的錯誤。(您可以看出多么容易犯這種錯誤)。
好在 `javax.swing.SwingUtilities` 類提供了幾個方便的方法 — `invokeAndWait()` 和 `invokeLater()`,它們可以消除某些線程問題。可以使用這兩個方法在 EDT 上同步或異步地執行操作。(關于 `SwingUtilities` 類的更多信息見 [參考資料](#resources))。`SwingBuilder` 讓我們很容易調用這兩個方法,還提供了第三個選擇:可以簡便地生成新線程以執行處理時間長的操作。
要想在 EDT 上執行同步調用 (`SwingUtilities.invokeAndWait()`),可以把調用放在 `edt{}` 閉包中。要想在 EDT 上執行異步調用 (`SwingUtilities.invokeLater()`),就把調用放在 `doLater{}` 閉包中。但是,我想讓您體驗一下第三個選擇:生成新線程來處理 `Search.byKeyword()` 方法調用。為此,需要把代碼放在 `doOutside{}` 閉包中,見清單 14:
##### 清單 14\. 使用 `doOutside` 閉包
```
def searchPanel = {
swingBuilder.panel(constraints: BorderLayout.NORTH){
searchField = textField(columns:15)
button(text:"Search", actionPerformed:{
doOutside{
resultsList.listData = Search.byKeyword(searchField.text)
}
} )
}
}
```
在像 Gwitter 這樣簡單的應用程序中,在 EDT 上執行 Web 服務調用很可能沒什么不好的效果。但是,如果把這樣的代碼拿給 Swing 專家看,他們會用鄙視的目光看您,就像是您在快車道里慢慢地開車,或者把車停在商店停車場的殘疾人專用車位上了。因為通過使用 `SwingBuilder` 很容易正確地處理線程,完全沒有理由不這么做。
既然解決了線程問題,下面就讓這個應用程序更漂亮一些。
* * *
## 給列表增加條紋效果
坦率地說,Gwitter 目前很難看。我要使用一些 HTML 代碼做兩個簡單的改進,讓外觀和感覺好一些。`JLabel` 可以顯示基本的 HTML。按清單 15 調整 Tweet.groovy 的 `toString()` 方法。`JList` 調用 `toString()` 方法顯示結果。
##### 清單 15\. 在 `toString()` 方法中返回 HTML
```
class Tweet{
String content
String published
String author
String toString(){
//return "${author}: ${content}"
return """<html>
<body>
<p><b><i>${author}:</i></b></p>
<p>${content}</p>
</body>
</html>"""
}
}
```
下一個改進略微有點復雜。一種常用的 GUI 技巧是給長的列表或表格加上條紋效果。用不同的顏色顯示奇數行和偶數行,這樣讀者更容易閱讀。我在搜索引擎中搜索了 _JList stripes_,采納了找到的第一篇文章中的建議。作者建議創建一個定制的 `DefaultListCellRenderer`。我完全贊同他的意見并按原樣借用他的示例代碼(完整的文章見 [參考資料](#resources))。
因為 Groovy 語法是 Java 語法的超集,所以可以把 Java 代碼復制到 Groovy 文件中,不需要修改。如果有功能全面的構建系統,可以編譯 Java 和 Groovy 代碼,那么只需把這段代碼留在 Java 文件中。但是,通過把代碼文件的擴展名改為 .groovy,我可以運行所有未編譯的 Gwitter 代碼。我再次利用了 Java 語言和 Groovy 之間的無縫集成。可以在 Groovy 應用程序中不加修改地使用任何 Java 解決方案。
創建文件 StripeRenderer.groovy,添加清單 16 中的代碼:
##### 清單 16\. 創建有條紋效果的 `CellRenderer`
```
import java.awt.*;
import javax.swing.*;
class StripeRenderer extends DefaultListCellRenderer {
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value,
index, isSelected, cellHasFocus);
if(index%2 == 0) {
label.setBackground(new Color(230,230,255));
}
label.setVerticalAlignment(SwingConstants.TOP);
return label;
}
}
```
有了 `StripeRenderer` 類之后,最后需要讓 `JList` 使用它。按清單 17 調整 `resultsPanel`:
##### 清單 17\. 在 `JList` 中添加定制的 `CellRenderer`
```
def resultsPanel = {
swingBuilder.scrollPane(constraints: BorderLayout.CENTER){
//resultsList = list()
resultsList =
list(fixedCellWidth: 380, fixedCellHeight: 75, cellRenderer:new StripeRenderer())
}
}
```
在命令提示上再次輸入 `groovy Gwitter`。搜索 _thirstyhead_ 應該會產生圖 16 所示的結果:
##### 圖 6\. 有條紋效果的結果

我可以花更多時間美化 Gwitter 的外觀和感覺,但是我希望您對大約 50 行 Swing 代碼(當然不包括支持類)所實現的效果印象深刻。
* * *
## 結束語
正如本文中指出的,Groovy 并不能降低 Swing 內在的復雜性,但是它可以顯著降低語法復雜性。這讓您能夠留出時間應付更重要的問題。
如果本文引起了您對 Groovy 和 Swing 的興趣,您應該好好研究一下 Griffon 項目(見 [參考資料](#resources))。它提供許多優于 Grails 項目的功能和慣例,但是它基于 `SwingBuilder` 和 Groovy 而不是 Spring MVC 和 Hibernate。這個項目仍然處于早期階段(到編寫本文時最新版本是 0.2),但是它已經很出色了,在 JavaOne 2009 上贏得了 Scripting Bowl for Groovy。另外,它提供的示例項目之一是 Greet,這是一個用 Groovy 實現的完整的 Twitter 客戶機。
下一次,我將在 Gwitter 中添加一些必備特性:發布新 Tweet 的功能。在此過程中,您將學習如何處理基本的 HTTP 身份驗證、執行 HTTP POST 以及使用與 `XmlSlurper` 相似的 `ConfigSlurper`。在此之前,我希望您探索應用 Groovy 的各種可能性。
* * *
## 下載
| 描述 | 名字 | 大小 |
| --- | --- | --- |
| 文章示例的源代碼 | [j-groovy09299.zip](http://www.ibm.com/developerworks/apps/download/index.jsp?contentid=447453&filename=j-groovy09299.zip&method=http&locale=zh_CN) | 24KB |
- 實戰 Groovy
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 2 部分
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 1 部分
- 實戰 Groovy: @Delegate 注釋
- 實戰 Groovy: 使用閉包、ExpandoMetaClass 和類別進行元編程
- 實戰 Groovy: 構建和解析 XML
- 實戰 Groovy: for each 剖析
- 實戰 Groovy: Groovy:Java 程序員的 DSL
- 實戰 Groovy: 關于 MOP 和迷你語言
- 實戰 Groovy: 用 curry 過的閉包進行函數式編程
- 實戰 Groovy: Groovy 的騰飛
- 實戰 Groovy: 在 Java 應用程序中加一些 Groovy 進來
- 實戰 Groovy: 用 Groovy 生成器作標記
- 實戰 Groovy: 用 Groovy 打造服務器端
- 實戰 Groovy: 使用 Groovy 模板進行 MVC 編程
- 實戰 Groovy: 用 Groovy 進行 JDBC 編程
- 實戰 Groovy: 用 Groovy 進行 Ant 腳本編程
- 實戰 Groovy: 用 Groovy 更迅速地對 Java 代碼進行單元測試
- alt.lang.jre: 感受 Groovy