精通 Grails: 创建自定义插件

本文内容包括:
在这个 “ 精通 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 的带来乐趣吧!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值