进入到 Groovy 风格的元编程世界。在运行时向类动态添加方法的能力 — 甚至 Java™ 类以及 final Java 类 — 强大到令人难以置信。不管是用于生产代码、单元测试或介于两者之间的任何内容,即使是最缺乏热情的 Java 开发人员也会对 Groovy 的元编程能力产生兴趣。
人们一直以来都认为 Groovy 是一种面向 JVM 的动态 编程语言。在这期 实战 Groovy 文章中,您将了解元编程 — Groovy 在运行时向类动态添加新方法的能力。它的灵活性远远超出了标准 Java 语言。通过一系列代码示例(都可以通过 下载 获得),将认识到元编程是 Groovy 的最强大、最实用的特性之一。
建模
程序员的工作就是使用软件建模真实的世界。对于真实世界中存在的简单域 — 比如具有鳞片或羽毛的动物通过产卵繁育后代,而具有毛皮的动物则通过产仔繁殖 — 可以很容易地使用软件对行为进行归纳,如清单 1 所示:
清单 1. 使用 Groovy 对动物进行建模
class ScalyOrFeatheryAnimal{ ScalyOrFeatheryAnimal layEgg(){ return new ScalyOrFeatheryAnimal() } } class FurryAnimal{ FurryAnimal giveBirth(){ return new FurryAnimal() } } |
|
不幸的是,真实的世界总是充满了例外和极端情况 — 鸭嘴兽既有皮毛,又通过产卵繁殖后代。我们精心考虑的每一项软件抽象几乎都存在与之相反的方面。
如果用来建模域的软件语言由于太过死板而无法处理不可避免的例外情况,那么最终的情形就像是受雇于一个小官僚机构的固执的公务员 — “对不起,Platypus 先生,如果要想我们的系统可以跟踪到您的话,您必须会生孩子。”
另一方面,Groovy 之类的动态语言为您提供了灵活性,使您能够更加准确地使用软件建模现实世界,而不是预先作出假设(并且通常是无效的),让现实向您妥协。如果 Platypus 类需要一个 layEgg() 方法,Groovy 可以满足要求,如清单 2 所示:
清单 2. 动态添加 layEgg() 方法
Platypus.metaClass.layEgg = {-> return new FurryAnimal() } def baby = new Platypus().layEgg() |
如果觉得这里举的有关动物的例子有些浅显,那么考虑 Java 语言中最常用的一个类:String。
Groovy 为 java.lang.String 提供的新方法
使用 Groovy 的乐趣之一就在于它添加到 java.lang.String 中的新方法。padRight() 和 reverse() 等方法提供了简单的 String 转换,如清单 3 所示。(有关 GDK 添加到 String 的所有新方法的列表的链接,见 参考资料。正如 GDK 在其首页中所说,“本文档描述了添加到 JDK 并更具 groovy 特征的方法。”)
清单 3. Groovy 添加到 String 的方法
println "Introduction".padRight(15, ".") println "Introduction".reverse() //output Introduction... noitcudortnI |
但是添加到 String 的方法并不仅限于简单的功能。如果 String 是一个组织良好的 URL,那么只需一行代码,您就可以将 String 转换为 java.net.URL 并返回 HTTP GET 请求的结果,如清单 4 所示:
清单 4. 发出 HTTP GET 请求
println "http://thirstyhead.com".toURL().text //output <html> <head> <title>ThirstyHead: Training done right.</title> <!-- snip --> |
再举一个例子,运行一个本地 shell 就像发出远程网络调用那么简单。一般情况下我将在命令提示中输入 ifconfig en0 以检查网卡的 TCP/IP 设置。(如果您使用的是 Windows® 而不是 Mac OS X 或 Linux®,那么尝试使用 ipconfig)。在 Groovy 中,我可以通过编程的方式完成同样的事情,参见清单 5:
清单 5. 在 Groovy 中发出一个 shell 命令
println "ifconfig en0".execute().text //output en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether 00:17:f2:cb:bc:6b media: autoselect status: inactive //snip |
我并没有说 Groovy 的优点在于您不能 使用 Java 语言做同样的事情。您当然可以。Groovy 的优点在于这些方法似乎可以直接添加到 String 类 — 这绝非易事,因为 String 是 final 类。(稍后将详细讨论这点)。清单 6 展示了 Java 中的相应内容 String.execute().text:
清单 6. 使用 Java 语言发出 shell 命令
Process p = new ProcessBuilder("ifconfig", "en0").start(); BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line = br.readLine(); while(line != null){ System.out.println(line); line = br.readLine(); } |
这看上去有点像在机动车辆管理局的各个窗口之间辗转,不是吗?“对不起,先生,要查看您请求的 String,首先需要去别处获得一个 BufferedReader。”
是的,您可以构建方便的方法和实用类来帮助将这个问题抽象出来,但是惟一的 com.mycompany.StringUtil 替代方法就是使用一个类来代替将方法直接添加到所属位置的行为:String 类。(当然就是 Platypus.layEgg()!)
那么 Groovy 究竟如何做 — 将新方法添加到无法扩展的类,或直接进行修改?要理解这一点,需要了解 closures 和 ExpandoMetaClass。
闭包和 ExpandoMetaClass
Groovy 提供了一种无害的但功能强大的语言特性 — 闭包 — 如果没有它的话,鸭嘴兽将永远无法下蛋。简单来说,闭包就是指定的一段可执行代码。它是一个未包含在类中的方法。清单 7 演示了一个简单闭包:
清单 7. 一个简单闭包
def shout = {src-> return src.toUpperCase() } println shout("Hello World") //output HELLO WORLD |
拥有一个独立的方法当然很棒,但是与将方法放入到现有类的能力相比,还是有些逊色。考虑清单 8 中的代码,其中并未创建接受 String 作为参数的方法,相反,我将方法直接添加到 String 类:
清单 8. 将 shout 方法添加到 String
String.metaClass.shout = {-> return delegate.toUpperCase() } println "Hello MetaProgramming".shout() //output HELLO METAPROGRAMMING |
未包含任何参数的 shout() 闭包被添加到 String 的 ExpandoMetaClass (EMC) 中。每个类 — 包括 Java 和 Groovy — 都包含在一个 EMC 中,EMC 将拦截对它的方法调用。这意味着即使 String 为 final,仍然可以将方法添加到其 EMC 中。因此,现在看上去仿佛 String 有一个 shout() 方法。