级别: 初级 Scott Davis, 创始人, ThirstyHead.com
2009 年 10 月 23 日 在这个 “
精通 Grails
” 系列中,Scott Davis 将向您展示如何创建您自己的 Grails 插件。一旦您了解创建插件有多么简单,您就会明白为什么现在有 250 多个 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.shortenurl
class 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.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 对象上与想要的日志级别相对应的方法,这些日志级别包括: 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.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. 确认插件安装成功
如果您访问 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 | HTTP |
|---|
参考资料 学习
获得产品和技术
-
Grails:下载 Grails 的最新版本。
-
cURL:cURL 默认安装在大多数 UNIX, Linux(r) 和 Mac OS X 系统上。您可以下载一个 Windows 版本和几乎其他所有 OS 版本。
讨论
关于作者
对本文的评价
|