`

实战 Groovy: 使用闭包、ExpandoMetaClass 和类别进行元编程

阅读更多
进入到 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()
  }
}


关于本系列

Groovy 是在 Java 平台上运行的一种现代编程语言。它能够与现有 Java 代码无缝集成,同时引入了各种生动的新特性,比如闭包和元编程。简单来讲,Groovy 是 Java 语言的 21 世纪版本。

将任何新工具整合到开发工具包中的关键是知道何时使用它以及何时将它留在工具包中。Groovy 的功能可以非常强大,但惟一的条件是正确应用于适当的场景。因此, 实战 Groovy 系列将探究 Groovy 的实际应用,以便帮助您了解何时以及如何成功使用它们。


不幸的是,真实的世界总是充满了例外和极端情况 — 鸭嘴兽既有皮毛,又通过产卵繁殖后代。我们精心考虑的每一项软件抽象几乎都存在与之相反的方面。

如果用来建模域的软件语言由于太过死板而无法处理不可避免的例外情况,那么最终的情形就像是受雇于一个小官僚机构的固执的公务员 — “对不起,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() 方法。

由于 Java 语言中不存在这种关系,因此 Groovy 必须引入一个新的概念:委托(delegate)。delegate 是 EMC 所围绕的类。

首先了解到方法调用包含在 EMC 中,然后了解了 delegate,您就可以执行任何有趣的操作。比如,注意清单 9 然后重新定义 String 的 toUpperCase() 方法:


清单 9. 重新定义 toUpperCase() 方法

String.metaClass.shout = {->
  return delegate.toUpperCase()
}

String.metaClass.toUpperCase = {->
  return delegate.toLowerCase()
}

println "Hello MetaProgramming".shout()


//output
hello metaprogramming



这个操作看上去仍然有些不严谨(甚至有些危险!)。尽管现实中很少需要修改 toUpperCase() 方法的行为,但是想象一下为代码单元测试带来的好处?元编程提供了快速、简单的方法,使潜在的随机行为具有了必然性。比如,清单 10 演示了 Math 类的静态 random() 方法被重写:


清单 10. 重写 Math.random() 方法

println "Before metaprogramming"
3.times{
  println Math.random()
}

Math.metaClass.static.random = {->
  return 0.5
}

println "After metaprogramming"
3.times{
  println Math.random()
}

//output
Before metaprogramming
0.3452
0.9412
0.2932
After metaprogramming
0.5
0.5
0.5



现在,想像一下对发出开销较高的 SOAP 调用的类进行单元测试。无需创建接口和去掉整个模拟对象的存根 — 您可以有选择地重写方法并返回一个简单的模拟响应。(您将在下一小节看到使用 Groovy 实现单元测试和模拟的例子)。

Groovy 元编程是一种运行时行为 — 这个行为从程序启动一直持续到程序运行。但是如果希望对元编程进行更多的显示该怎么做(对于编写单元测试尤其重要)?在下一小节,您将了解揭秘元编程的秘密。





解密元编程

清单 11 封装了我在 GroovyTestCase 中编写的演示代码,这样就可以更加严格地对输出进行测试。(参见 “实战 Groovy: 用 Groovy 更迅速地对 Java 代码进行单元测试” 了解更多有关使用 GroovyTestCase 的信息)。


清单 11. 使用单元测试分析元编程

class MetaTest extends GroovyTestCase{

  void testExpandoMetaClass(){
    String message = "Hello"
    shouldFail(groovy.lang.MissingMethodException){
      message.shout()
    }

    String.metaClass.shout = {->
      delegate.toUpperCase()
    }

    assertEquals "HELLO", message.shout()

    String.metaClass = null
    shouldFail{
      message.shout()
    }
  }
}



在命令提示中输入 groovy MetaTest 以运行该测试。

注意,只需将 String.metaClass 设置为 null,就可以取消元编程。

但是,如果您不希望 shout() 方法出现在所有 String 中该怎么办呢?您可以仅调整单一实例的 EMC(而不是类),如清单 12 所示:


清单 12. 对单个实例进行元编程

void testInstance(){
  String message = "Hola"
  message.metaClass.shout = {->
    delegate.toUpperCase()
  }

  assertEquals "HOLA", message.shout()
  shouldFail{
    "Adios".shout()
  }
}



如果准备一次性添加或重写多个方法,清单 13 展示了如何以块的方式定义新方法:


清单 13. 一次性对多个方法进行元编程

void testFile(){
  File f = new File("nonexistent.file")
  f.metaClass{
    exists{-> true}
    getAbsolutePath{-> "/opt/some/dir/${delegate.name}"}
    isFile{-> true}
    getText{-> "This is the text of my file."}
  }

  assertTrue f.exists()
  assertTrue f.isFile()
  assertEquals "/opt/some/dir/nonexistent.file", f.absolutePath
  assertTrue f.text.startsWith("This is")
}



注意,我再也不关心文件是否存在于文件系统中。我可以将它发送给这个单元测试中的其他类,并且它表现得像一个真正的文件。当 f 变量在测试结束时超出范围之后,还会执行定制行为。

尽管 ExpandoMetaClass 十分强大,但是 Groovy 提供了另一种元编程方法,使用了它独有的一组功能:类别(category)。





类别和 use 块

解释 Category 的最佳方法就是了解它的实际运行。清单 14 演示了使用 Category 来将 shout() 方法添加到 String:


清单 14. 使用一个 Category 进行元编程

class MetaTest extends GroovyTestCase{
  void testCategory(){
    String message = "Hello"
    use(StringHelper){
      assertEquals "HELLO", message.shout()
      assertEquals "GOODBYE", "goodbye".shout()
    }

    shouldFail{
      message.shout()
      "foo".shout()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}



如果曾经从事过 Objective-C 开发,那么应当对这个技巧感到熟悉。StringHelper Category 是一个普通类 — 它不需要扩展特定的父类或实现特殊的接口。要向类型为 T 的特定类添加新方法,只需定义一个静态方法,它接受类型 T 作为第一个参数。由于 shout() 是一个接受 String 作为第一个参数的静态方法,因此所有封装到 use 块中的 String 都获得了一个 shout() 方法。

那么,什么时候应该选择 Category 而不是 EMC?EMC 允许您将方法添加到某个类的单一实例或所有实例中。可以看到,定义 Category 允许您将方法添加到特定 实例中 — 只限于 use 块内部的实例。

虽然 EMC 允许您动态定义新行为,然而 Category 允许您将行为保存到独立的类文件中。这意味着您可以在不同的情况下使用它:单元测试、生产代码,等等。定义单独类的开销在重用性方面获得了回报。

清单 15 演示了对同一个 use 块同时使用 StringHelper 和新创建的 FileHelper:


清单 15. 在 use 块中使用多个类别

class MetaTest extends GroovyTestCase{
  void testFileWithCategory(){
    File f = new File("iDoNotExist.txt")
    use(FileHelper, StringHelper){
      assertTrue f.exists()
      assertTrue f.isFile()
      assertEquals "/opt/some/dir/iDoNotExist.txt", f.absolutePath
      assertTrue f.text.startsWith("This is")

      assertTrue f.text.shout().startsWith("THIS IS")
    }

    assertFalse f.exists()
    shouldFail(java.io.FileNotFoundException){
      f.text
    }
  }
}


class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}


class FileHelper{
static boolean exists(File f){
   return true
}

static String getAbsolutePath(File f){
   return "/opt/some/dir/${f.name}"
}

static boolean isFile(File f){
   return true
}

static String getText(File f){
   return "This is the text of my file."
}
}



但是有关类别的最有趣的一点是它们的实现方式。EMC 需要使用闭包,这意味着您只能在 Groovy 中实现它们。由于类别仅仅是包含静态方法的类,因此可以用 Java 代码进行定义。事实上,可以在 Groovy 中重用现有的 Java 类 — 对元编程来说总是含义不明的类。

清单 16 演示了使用来自 Jakarta Commons Lang 包(见 参考资料)的类进行元编程。org.apache.commons.lang.StringUtils 中的所有方法都一致地遵守 Category 模式 — 静态方法接受 String 作为第一个参数。这意味着可以使用现成的 StringUtils 类作为 Category。


清单 16. 使用 Java 类进行元编程

import org.apache.commons.lang.StringUtils

class CommonsTest extends GroovyTestCase{
  void testStringUtils(){
    def word = "Introduction"

    word.metaClass.whisper = {->
      delegate.toLowerCase()
    }

    use(StringUtils, StringHelper){
      //from org.apache.commons.lang.StringUtils
      assertEquals "Intro...", word.abbreviate(8)

      //from the StringHelper Category
      assertEquals "INTRODUCTION", word.shout()

      //from the word.metaClass
      assertEquals "introduction", word.whisper()
    }
  }
}

class StringHelper{
  static String shout(String self){
    return self.toUpperCase()
  }
}



输入 groovy -cp /jars/commons-lang-2.4.jar:. CommonsTest.groovy 以运行测试(当然,您需要修改在系统中保存 JAR 的路径)。






元编程和 REST


为了不让您产生元编程只对 单元测试有用的误解,下面给出了最后一个例子。回忆一下 “实战 Groovy:构建和解析 XML” 中可以预报当天天气情况的 RESTful Yahoo! Web 服务。通过将上述文章中的 XmlSlurper 技巧与本文的元编程技巧结合起来,您就可以通过 10 行代码查看任何 ZIP 码所代表的位置的天气信息,如清单 17 所示:


清单 17. 添加一个 weather 方法

String.metaClass.weather={->
  if(!delegate.isInteger()){
    return "The weather() method only works with zip codes like '90201'"
  }
  def addr = "http://weather.yahooapis.com/forecastrss?p=${delegate}"
  def rss = new XmlSlurper().parse(addr)
  def results = rss.channel.item.title
  results << "\n" + rss.channel.item.condition.@text
  results << "\nTemp: " + rss.channel.item.condition.@temp
}

println "80020".weather()


//output
Conditions for Broomfield, CO at 1:57 pm MDT
Mostly Cloudy
Temp: 72



可以看到,元编程提供了极好的灵活性。您可以使用本文介绍的任何(或所有)技巧来向任意数量的类添加方法。

分享到:
评论

相关推荐

    microservices-spring-boot-groovy:使用 Spring Boot 和 Groovy 构建微服务

    微服务-spring-boot-groovy 使用 Spring Boot 和 Groovy 构建微服务创建这些项目是为了在当地的达拉斯 Groovy Grails 用户组会议上展示微服务架构这些服务使用您需要安装才能开始使用的各种外部服务。 您将需要安装 ...

    Flutter 出现Could not download groovy-all.jar (org.codehaus.groovy:groovy-all:2.4.15)

    在一次代码拉取中,出现了以下问题:Could not download groovy-all.jar (org.codehaus.groovy:groovy-all:2.4.15) 详细的报错信息如下: // 报错信息如下 Could not resolve all files for configuration ':jcore-...

    groovy-all-2.4.12.jar

    最近的groovy-all-2.4.12.jar包供使用,解决as编译时报错问题

    groovy-3.0.9-API文档-中文版.zip

    Maven坐标:org.codehaus.groovy:groovy:3.0.9; 标签:groovy、codehaus、jar包、java、中文文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码...

    groovy:Apache Groovy:用于JVM平台的功能强大的多面编程语言

    groovy:Apache Groovy:用于JVM平台的功能强大的多面编程语言

    实战groovy.rar

    实战groovy.rar

    groovy-2.5.1-API文档-中文版.zip

    Maven坐标:org.codehaus.groovy:groovy:2.5.1; 标签:codehaus、groovy、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码...

    groovy-3.0.9-API文档-中英对照版.zip

    Maven坐标:org.codehaus.groovy:groovy:3.0.9; 标签:groovy、codehaus、jar包、java、中英对照文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的...

    groovy-all-2.4.15.jar

    org.gradle.api.resources.ResourceException: Could not get resource 'https://jcenter.bintray.com/org/codehaus/groovy/groovy-all/2.4.15/groovy-all-2.4.15.jar'. ...重启 Android Studio 及可使用。

    groovy-2.5.1-API文档-中英对照版.zip

    Maven坐标:org.codehaus.groovy:groovy:2.5.1; 标签:codehaus、groovy、中英对照文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的...

    ratpack-secure-groovy:使用Groovy在Ratpack中进行安全的REST API演示

    使用Groovy保护Ratpack中的REST API这是Groovy中带有Ratpack的休息服务的基于令牌安全性的简单实现蒙哥用户和令牌由MongoDB支持。 在application.properties中设置您的MongoDB连接信息使用要运行此命令,只需使用...

    groovy-all-2.4.15.jar.zip

    groovy-all-2.4.15.jar文件,MAC使用时需存放在/Users/用户名/.gradle/caches/jars-3/某一缓存目录下,找不到就都看一下,我遇到的问题是缓存目录中下载的是2.4.17版本,应该跟gradle版本升级有关

    groovy-3.0.7.msi

    groovy

    groovy-all-2.4.13-API文档-中文版.zip

    Maven坐标:org.codehaus.groovy:groovy-all:2.4.13; 标签:all、groovy、codehaus、jar包、java、API文档、中文版; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化...

    groovy2.4.3

    是一个基于Java虚拟机的敏捷动态语言。 构建在强大的Java语言之上 并添加了从Python,Ruby和Smalltalk等语言中学到的诸多特征。...直接编译成Java字节码,这样可以在任何使用Java的地方 使用Groovy。

    awesome-groovy:精妙的groovy库,框架和资源的精选列表

    awesome-groovy:精妙的groovy库,框架和资源的精选列表

    groovy-all-2.4.5-API文档-中文版.zip

    Maven坐标:org.codehaus.groovy:groovy-all:2.4.5; 标签:codehaus、groovy、all、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,...

    Groovy v2.4.13官方版

     Groovy是JVM的一个替代语言(替代是指可以用 Groovy 在Java平台上进行Java 编程),使用方式基本与使用 Java代码的方式相同,该语言特别适合与Spring的动态语言支持一起使用,设计时充分考虑了Java集成,这使 ...

    apache-groovy-sdk-3.0.3.zip

    最新版本的groovy3.0.3,对groovy及gradle使用有兴趣的可以下来试用,为大家提供优秀资源,自己也赚点积分。

    groovy(10)-闭包委托策略1

    groovy(10)-闭包委托策略 /*闭包的三个重要变量:this,owner,delegate区别在于:this代表闭包定义处最近的对象(不包含闭包),ow

Global site tag (gtag.js) - Google Analytics