# 实战 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`
![](https://box.kancloud.cn/2016-05-11_5732f381081f3.jpg)
清单 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`
![](https://box.kancloud.cn/2016-05-11_5732f38117f87.jpg)
注意,在 [清单 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 菜单
![](https://box.kancloud.cn/2016-05-11_5732f3812a910.jpg)
再看看 [清单 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\. 搜索结果
![](https://box.kancloud.cn/2016-05-11_5732f3813ef18.jpg)
现在可以宣布成功了,但是我想先解决两个问题。第一个问题是搜索按钮的 `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\. 有条纹效果的结果
![](https://box.kancloud.cn/2016-05-11_5732f3815ddd5.jpg)
我可以花更多时间美化 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