這個 精通 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 的帶來樂趣吧!
本文配套源碼