精通Grails:创建自定义插件

这个 精通 Grails 系列文章主要关注智能代码重用。如果您需要在多个地方复制和粘贴相同的 GroovyServer Pages (GSP) 代码段,您就可以创建一个部分模板或一个自定义 TagLib。如果您发现有一 两个方法在多个控制器或域类中很普遍,您就可以使用 ExpandoMetaClass 创建一个抽象父类来直接扩展 或嫁接这些方法。如果您有某个共享应用程序功能,那么可以将它重构为一个服务或一个自定义编解码器 。

关于本系列

Grails 是一个现代的 Web 开发框架,它将熟悉的 Java™ 技术(比 如 Spring 和 Hibernate)和最新的实践(比如约定优于配置)结合起来。用 Groovy 编写的 Grails 使 您可以与遗留的 Java 代码无缝集成,同时又添加了脚本语言的灵活性和动态性。学习了 Grails 之后, 您将对 Web 开发有新的看法。

但这些都是微观层面上的东西。如果在宏观层面有某个共享功能, 需要控制器和域类、服务和编解码器,以及一个典型的 Grails 的其他组件的联合和协调,那又该怎么办 呢?如前所述,答案就是插件。

在 “精通 Grails:了解插件” 中,我们学习了一个 现有插件:Searchable。Grails Plugins 门户网站有 250 多个插件可用(参见 参考资料)。这个数字 还在不断增加,原因是通过插件扩展现有的 Grails 应用程序是 Grails 的核心理念。在本文中,您将学 习如何构建自己的自定义插件。示例插件的源代码可以从 下载 获取。

ShortenUrl 插件简介

测试至上

测试您的 Grails 应用程序总是很重要,在创建插件时,测试尤其重要。插件中 的缺陷的负面影响可能会成倍放大,损害安装该插件的应用程序。您将看到,本文将重点关注测试。

在这个 Twitter.com 和手机消息通讯时代,许多长 URL 不能满足消息上设置的 140 个字符的限 制,这是一件麻烦事!幸运的是,有几个 URL 缩短服务强烈要求作为自定义插件集成到 Grails 中。

要创建一个自定义插件,必须略微更改 Grails 例程。您必须输入 grails create-plugin(见清单 1 ),而不是像往常一样输入 grails create-app。(一定要在一个新的空目录中输入这个命令,而不是 在一个现有 Grails 目录中输入。本文末尾将介绍如何集成这个新插件和一个现有 Grail 应用程序)。

清单 1. 创建一个自定义插件

$ grails create-plugin shortenurl

生成的目录结构与一个典型的 Grails 应用程序一致。但是,根目录中有一个文件将这个项目识别为 一个插件:ShortenurlGrailsPlugin.groovy。清单 2 显示了一段代码:

清单 2. 插件配置文件

class ShortenurlGrailsPlugin {   // the plugin version   def version = "0.1"   // the version or versions of Grails the plugin is designed for   def grailsVersion = "1.1.1 > *"   // the other plugins this plugin depends on   def dependsOn = [:]   // resources that are excluded from plugin packaging   def pluginExcludes = [       "grails-app/views/error.gsp"   ]   // TODO Fill in these fields   def author = "Your name"   def authorEmail = ""   def title = "Plugin summary/headline"   def description = '''//Brief description of the plugin.'''   //snip}

这个文件包含插件元数据:版本号、插件附属的 Grails 的版本号、插件附属的其他插件等。(要查 看包含配置文件详细信息的在线文档,请参见 参考资料)。

如果您想允许其他开发人员从 Plugins 门户网站下载这个插件,应该填写作者信息和具有吸引力的说 明。每当您将插件签入公共 Subversion 存储库,文件的内容将被读取并自动显示在 Grails Web 站点上 。(要了解关于发表您的插件的更多信息,请参见 参考资料)。在本文中,这个插件将作为一个私有插 件,因此,填写作者信息就不那么重要了。

即使这个 ShortenUrl 插件不需要对 ShortenurlGrailsPlugin.groovy 进行任何更改,但这并不代表 您的工作已经完成了。现在目录结构已经就绪,下一步就是编写实现。

创建 TinyUrl 类

TinyUrl.com 是一个流行的 URL-shortening 服务。某人提交一个长 URL 请求缩短后,它将针对后续 请求在后台将其存储为一个正式的缩短 URL。例如,访问该站点,输入 http://www.grails.org/The+Plug-in+Developers+Guide,然后单击 Make TinyURL! 按钮。生成的缩短 URL — http://tinyurl.com/73495c — 是原长度的一半,如图 1 所示。

图 1. TinyURL.com 缩短一个 URL

现在您了解了 TinyURL.com 的工作方式,下面可以关注如何将这个网站的底层服务和 ShortenUrl 插 件集成起来了。在您的 Web 浏览器中输入以下内容:

http://tinyurl.com/api-create.php?url=http://www.grails.org/The+Plug- in+Developers+Guide

这个 Web 服务界面只返回指定页面的缩短的 URL,而不是 HTML。

下一步是将您的新发现封装到 Groovy 类中。这个类是一个 Plain Old Groovy Object (POGO),正如 它的名称所示,它不是服务、控制器或任何其他具有特殊目的的 Grails 组件。因此,放置它的最好位置 是 src/groovy。在 src/groovy 下创建一个 org/grails/shortenurl 目录,然后创建 TinyUrl.groovy 并添加清单 3 中的代码:

清单 3. TinyUrl 实用程序类

package org.grails.shortenurlclass TinyUrl{  static String shorten(String longUrl){   def addr = "http://tinyurl.com/api-create.php?url=${longUrl}"   return addr.toURL().text  }}

插件中的包

将插件的类放在一个包中是一种很好的实践,这极大地减小了与用户的 Grails 项目中的现有类造成 冲突的几率。

还可以打包域类、控制器等。对于简单的项目,这种不太常见的实践会增加不必要的复杂性,但经验 丰富的 Grails 开发人员非常信任这种实践。

测试 TinyUrl 类

将代码用于生产前,应该进行相应的测试,不是吗?由于您要进行一个实时 Web 调用,因此这应该是 一个集成测试。在 test/integration 下创建此前创建过的相同的 org/grails/shortenurl 目录结构。 创建 TinyUrlTests.groovy 并添加清单 4 中的代码。(在这个简单的例子中,宣称很小的 URL 竟然比 它要编码的原始 URL 还要长。这非常有趣)。

清单 4. 测试 TinyUrl 类

package org.grails.shortenurlclass TinyUrlTests extends GroovyTestCase{  def transactional = false  void testShorten(){   def shortUrl = TinyUrl.shorten("http://grails.org")   assertEquals "http://tinyurl.com/3xfpkv", shortUrl  }}

注意集成测试中的 def transactional = false 这一行。如果省略这一行,您将收到令人讨厌的错误 消息,如清单 5 所示。

清单 5. 测试没有设置 def transactional = false 时收到的错误消息

Error running  integration tests: java.lang.RuntimeException:There is no test TransactionManager definedand integration test ${test.name} does not set transactional = false

Grails 试图在数据库事务中包含所有测试。在普通的 Grails 应用程序中,这不成问题。但是您在一 个插件中而不是在应用程序中,因此您不能假定存在这样一个数据库。您可以安装 Hibernate 插件,或 者按照错误消息的指示在集成测试中设置 def transactional = false。

输入 grails test-app 并验证您的测试是否通过。

我还要实现一个 URL 缩短服务,以便这个插件的用户可以选择其中一个服务。

创建 IsGd 类

这个 Is.Gd(读作 is good)服务号称能够提供比 TinyUrl.com 更短的域名和编码 URL。访问 http://is.gd 试验这个 Web 界面。

为了再次表示我这种长短反差的偏好,我将借此机会向您展示我在 TinyUrl.groovy 中使用过的那个 两行方法(参见 清单 3)的更长实现。如果服务失败,这个实现将提供更多信息以便做出相应反应。在 src/groovy/org/grails/shortenurl 中创建 IsGd.groovy,如清单 6 所示。

清单 6. IsGd 实用程序类

package org.grails.shortenurlclass IsGd{  static String shorten(String longUrl){   def addr = "http://is.gd/api.php?longurl=${longUrl}"   def url = addr.toURL()   def urlConnection = url.openConnection()   if(urlConnection.responseCode == 200){    return urlConnection.content.text   }else{    return "An error occurred: ${addr}/n" +    "${urlConnection.responseCode} : ${urlConnection.responseMessage}"   }  }}

如您所见,清单 6 的响应代码为 200 —— 表示 OK 的 HTTP 响应代码(参见 参考资料 了解关于 HTTP 响应代码的更多信息)。为简便起见,调用失败时仅返回错误消息。但使用现成的扩展结 构,您可以多次重新尝试调用或将故障转移到另一个 URL 缩短服务,从而使这个方法更健壮。

在 test/integration/org/grails/shortenurl 目录中创建对应的 IsGdTests.groovy 文件,如清单 7 所示 。输入 grails test-app 并确认 IsGd 类工作正常。

清单 7. 测试 IsGd 类package org.grails.shortenurlclass IsGdTests extends  GroovyTestCase{ def transactional = false void testShorten (){  def shortUrl = IsGd.shorten("http://grails.org")  assertEquals  "http://is.gd/2oCZR", shortUrl } void testBadUrl(){  def shortUrl = IsGd.shorten("IAmNotAValidUrl")  println shortUrl  assertTrue shortUrl.startsWith("An error occurred:") }}

传递 IAmNotAValidUrl 时,IsGd 服务将失败。要了解该服务是如何失败的详细信息,建 议您跳到命令行并使用 curl 命令,如清单 8 所示。(cURL 实用程序是 UNIX®/Linux®/Mac OS X 上的原生命令,可以下载 Windows® 版本,参见 参考资料)。在浏览器中测试错误的 URL 可以看 到错误消息,但看不到错误代码。使用 cURL,您可以清楚地看到,Web 服务返回一个 500 代码,而不是 预期的 200。

清单 8. 使用 curl 查看失败 Web 服务类的细节

$ curl --verbose  "http://is.gd/api.php?longurl=IAmNotAValidUrl"* About to connect() to is.gd port 80 (#0)*  Trying 78.31.109.147... connected* Connected to is.gd (78.31.109.147) port 80 (#0)> GET /api.php?longurl=IAmNotAValidUrl HTTP/1.1> User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) libcurl/7.16.3          OpenSSL/0.9.7l zlib/1.2.3> Host: is.gd> Accept: */*>< HTTP/1.1 500 Internal Server Error< X-Powered-By: PHP/5.2.6 < Content-type: text/html; charset=UTF-8< Transfer-Encoding: chunked< Date: Wed, 19 Aug 2009 17:33:04 GMT< Server: lighttpd/1.4.22 <* Connection #0 to host is.gd left intact* Closing connection #0Error: The URL entered was not valid.

现在这个插件的核心功能已经实现并经过测试,您应该创建一个方便的服务,以一种 Grails 友好的 方式公开这两个实用程序类。

创建 ShortenUrl 服务

要创建一个服务,输入 grails create-service ShortenUrl。将清单 9 中的代码添加到 grails- app/services/ShortenUrlService.groovy。

清单 9. ShortenUrl 服务

import org.grails.shortenurl.*class ShortenUrlService {   boolean transactional = false   def tinyurl(String longUrl) {    return TinyUrl.shorten(longUrl)   }   def isgd(String longUrl) {    def shortUrl = IsGd.shorten(longUrl)    if(shortUrl.contains("error")){     log.error(shortUrl)    }    return shortUrl   }}

与前面的集成测试相似,确保将 transactional 标记设置为 false。这些调用不涉及任何数据库,所 以不必将它们封装到一个事务中。

注意,isgd() 方法将记录任何企图缩短一个无效 URL 的日志。所有 Grails 工件将在运行时使用一 个 log 对象注入。可以调用 log 对象上与想要的日志级别相对应的方法,这些日志级别包括: debug、 info 和 error 等(参见 参考资料 了解关于日志记录的更多信息)。您稍后将会看到,编写单元测试时 ,处理这个注入的 log 对象需要一个额外步骤。

当 Grails 为您创建服务时,它将把相应的测试添加到 test/unit 目录。通常,您需要将 ShortenUrlServiceTests.groovy 移动到 test/integration 目录,因为在语义上,它是一个集成测试, 而不是一个单元测试 — 依赖外部资源测试服务。但现在,您应将它保留在 test/unit 目录中,以便我 能够向您展示几个单元测试技巧。将清单 10 中的代码添加到 ShortenUrlServiceTests.groovy。

清单 10. 测试 ShortenUrl 服务

import grails.test.*class ShortenUrlServiceTests extends GrailsUnitTestCase {   def transactional = false   def shortenUrlService   protected void setUp() {     super.setUp()     shortenUrlService = new ShortenUrlService()   }   protected void tearDown() {     super.tearDown()   }   void testTinyUrl() {    def shortUrl = shortenUrlService.tinyurl("http://grails.org")    assertEquals "http://tinyurl.com/3xfpkv", shortUrl   }   void testIsGd() {    def shortUrl = shortenUrlService.isgd("http://grails.org")    assertEquals "http://is.gd/2oCZR", shortUrl   }   void testIsGdWithBadUrl() {    def shortUrl = shortenUrlService.isgd("IAmNotAValidUrl")    assertTrue shortUrl.startsWith("An error occurred:")   }}

注意,将 transactional 标志设置为 false 后,我们声明了 shortenUrlService 变量。然后在 setUp() 方法中初始化服务。为每个服务调用 setUp() 和 tearDown() 方法。

如果这是一个集成测试,则不会出现错误。但由于这是一个单元测试,testIsGdWithBadUrl() 方法失 败并显示错误消息:No such property: log for class: ShortenUrlService。在 Web 浏览器中打开 test/reports/html/index.html,您将看到如图 2 所示的错误消息。

图 2. 注入的 log 对象导致单元测试失败

如上所示,log 对象并没有注入服务中以进行单元测试。(记住:单元测试意味着完全隔离运行)。 好在解决这个问题只需在 setUp() 方法中添加一行 — mockLogging(ShortenUrlService) — 如清单 11 所示。

清单 11. 模拟注入的 log 对象

protected void setUp() {   super.setUp()   mockLogging(ShortenUrlService)   shortenUrlService = new ShortenUrlService()}

mockLogging() 方法将一个模拟 log 对象注入到服务中。这个模拟记录器将它的输出发送到 System.out 而不是任何已定义的 log4j 输出器。要查看输出(如图 3 所示),再次输入 grails test -app,单击 ShortenUrlServiceTests 的 HTML 报告页面底部的 System.out 链接。

图 3. 模拟记录器的输出

您还可以为这个插件集成大量其他 Grails 工件 — 一个自定义 TagLib 以缩短 GSP 中的 URL,一个 自定义编解码器 — 但现在您已经充分了解一个插件可以提供的内容,在这里就不一一演示了。在下一个 小节中,我们将把这个插件原样打包并集成到另一个 Grails 项目中。

打包并部署插件

要准备一个完整的 Grails 应用程序以便部署,通常需要输入 grails war。但对于插件,则应输入 grails package-plugin。这样,您的项目中将生成一个 grails-shortenurl-0.1.zip 文件。

回想一下,“精通 Grails:了解插件” 介绍过,所有 Grails 插件都作为 ZIP 文件分发。查看一下 home 目录中的 .grails/1.1.1/plugins 目录,您将看到类似的插件名称,比如 grails-hibernate- 1.1.1.zip 和 grails-searchable-0.5.5.zip。

假如 ShortenUrl 是一个公共插件,您可以输入 grails release-plugin 将您的更改提交到 Grails Plugins 门户网站。然后,任何人都可以输入 grails install-plugin shortenurl 将它集成到他们的项 目中。您也可以在本地轻松安装私有插件,只需提供 ZIP 文件在您的本地文件系统上的完整路径。

要测试这一点,在 shortenurl 目录外创建一个新的空目录。输入 grails create-app foo 创建一个 简单的应用程序。切换到 foo 目录并输入 grails install-plugin /local/path/to/grails- shortenurl-0.1.zip,当然,要用实际插件路径替换其中的路径。您将看到类似于清单 12 的输出:

清单 12. 安装一个本地插件

$ grails install-plugin /code/grails-shortenurl- 0.1.zipWelcome to Grails 1.1.1 - http://grails.org/Licensed under Apache Standard License 2.0Grails home is set to: /opt/grailsBase DirecTory: /code/fooRunning script. /opt/grails/scripts/InstallPlugin.groovy Environment set to development    [copy] Copying 1 file to /Users/sdavis/.grails/1.1.1/plugins    Installing plug-in shortenurl-0.1    [mkdir] Created dir:    /Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1    [unzip] Expanding:    /Users/sdavis/.grails/1.1.1/plugins/grails-shortenurl-0.1.zip into    /Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1Executing shortenurl-0.1 plugin post-install script. ...Plugin shortenurl-0.1 installed 

如您所见,本地、私有插件的生命周期和公共插件的相同。

在文本编辑器中打开 foo/application.properties 文件,确认 plugins.shortenurl 如清单 13 所 示。

清单 13. 确认插件出现在 application.properties 中

#utf-8#Wed Aug 19 14:38:24 MDT 2009app.version=0.1app.servlet.version=2.4 app.grails.version=1.1.1plugins.hibernate=1.1.1plugins.shortenurl=0.1app.name=foo

安装插件后,应该确认它能够正常工作。输入 grails create-controller test。打开 grails- app/controllers/TestController.groovy 并添加清单 14 中的代码。

清单 14. 将服务注入到控制器中

class TestController {   def shortenUrlService   def index = {    render "This is a test for the ShortenUrl plug-in" +        "Type test/tinyurl?q=http://grails.org to try it out."   }   def tinyurl = {    render shortenUrlService.tinyurl(params.q)   }}

注意,def shortenUrlService 将服务注入到控制器中。输入 grails run-app 启动应用程序。在 Web 浏览器中访问 http://localhost:9090/foo/test/tinyurl?q=http://grails.org,应该可以看到如 图 4 所示的结果。

图 4. 确认插件安装成功

如果您访问 http://tinyurl.com/3xfpkv,肯定会进入 grails.org 页面。

结束语

如您所见,创建 Grails 插件与创建典型的 Grails 应用程序没有多大区别。创建插件时,应该输入 grails create-plugin 而不是 grails create-app,应该输入 grails package-plugin 而不是 grails war。除了在 GrailsPlugin.groovy 描述符文件中添加的细节不同外,所有中间步骤(创建服务和编写测 试等)都是相同的。

本文通过 mockLogging() 方法简单探索了 Grails 单元测试的模拟功能。在下一篇文章中,我将展示 其他几种极其有用的模拟方法: mockDomain() 和 mockForConstraintsTests()等。在此之前,请尽情享 受 Grails 的带来乐趣吧!

本文配套源码

人的价值,在遭受诱-惑的一瞬间被决定

精通Grails:创建自定义插件

相关文章:

你感兴趣的文章:

标签云: