精通 Grails: 创建自定义插件
 

2009-11-27 作者:Scott Davis 来源:IBM

 
本文内容包括:
在这个 “ 精通 Grails ” 系列中,Scott Davis 将向您展示如何创建您自己的 Grails 插件。一旦您了解创建插件有多么简单,您就会明白为什么现在有 250 多个 Grails 插件可用,而且这个数字还在增加。

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

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

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

ShortenUrl 插件简介

在这个 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
图 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.shortenurl

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

测试 TinyUrl

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

清单 4. 测试 TinyUrl
 
				
package org.grails.shortenurl

class 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 defined 
and 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.shortenurl

class 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.shortenurl

class 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 #0
Error: 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 对象上与想要的日志级别相对应的方法,这些日志级别包括: debuginfoerror 等(参见 参考资料 了解关于日志记录的更多信息)。您稍后将会看到,编写单元测试时,处理这个注入的 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 对象导致单元测试失败
图 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. 模拟记录器的输出
图 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.zip
Welcome to Grails 1.1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails

Base Directory: /code/foo
Running 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.1
Executing 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 2009
app.version=0.1
app.servlet.version=2.4
app.grails.version=1.1.1
plugins.hibernate=1.1.1
plugins.shortenurl=0.1
app.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. 确认插件安装成功
图 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 的带来乐趣吧!

下载

描述 名字 大小 下载方法
源代码
j-grails09159.tar
820KB

参考资料

学习
  • 精通 Grails :阅读这个系列的其他文章,进一步了解 Grails 以及如何使用它。
  • Grails:访问 Grails Web 站点。
  • Grails Plugins:访问这个 Grails 插件的门户网站,获得有关可用于 Grails 框架的最新插件的信息。
  • 插件开发人员指南:这是针对 Grails 插件开发人员的在线文档。
  • 创建插件:获取关于创建、分发和安装 Grails 插件的更多信息。
  • TinyURL:阅读关于 TinyURL 的 Wikipedia 文章。
  • HTTP 状态代码:Wikipedia 提供 HTTP 状态代码的完整列表。
  • 日志:了解关于 Grails 中的日志配置的更多信息。
  • Grails Framework Reference Documentation:Grails 宝典。
  • Groovy Recipes (Scott Davis,Pragmatic Programmers,2008 年):从 Scott Davis 最新撰写的书籍中了解更多关于 Groovy 和 Grails 的信息。
  • 实战 Groovy :这个 developerWorks 系列专门探索 Groovy 的实际使用方法,并教您何时以及如何应用它们。
  • 技术书店:浏览关于这个主题和其他技术主题的图书。
  • developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
获得产品和技术
  • Grails:下载 Grails 的最新版本。
  • cURL:cURL 默认安装在大多数 UNIX, Linux(r) 和 Mac OS X 系统上。您可以下载一个 Windows 版本和几乎其他所有 OS 版本。
讨论
火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。

资源网站: UML软件工程组织