在開始階段,精通 Grails 主要著眼於核心 Grails 功能。對如何將基礎部件組合在一起了解得越多 ,將其結合起來構建一個完善的產品應用程序就會變得越容易。盡管我前面多次提到過插件,但我均有意 回避了對插件做深入的介紹。現在,該是介紹的時候了。
在接下來的幾篇系列文章中,我將與您一起探索 Grails 插件系統。最早,Grails 平台的構建對可插 入性是有所考慮的。正因為有了這個雖小卻十分重要的考慮,我們才能很方便地利用上百個預捆綁的功能 塊。
在本文寫作之時,清單 1 所示的 Groovy 腳本已經能夠返回 225 個插件。(有關此腳本如 何工作的詳細信息,請參見 “實戰 Groovy:構建和解析 XML”。)
清單 1. 計算可用 Grails 插件的簡單 Groovy 腳本
def addr = "http://plugins.grails.org/.plugin-meta/plugins-list.xml"
def plugins = new XmlSlurper().parse(addr)
def count = 0
plugins.plugin.each{
println it.@name
count++
}
println "Total number of plugins: ${count}"
要獲得一個更為友好的列表,可以在命令提示符後鍵入 grails list-plugins 或訪問 Grails Plugins 站點。
何為插件?
老練的 Java™ 開發人員都是一些精明的探尋者和收集者。他們從不夢想著去編寫自已的日志庫 ;而是簡單地把 log4j JAR 放入其類路徑。需要一個 XML 解析器嗎?那好,將 Xerces JAR 添加到您的 項目中即可。這些可插入的功能塊是面向對象編程的可重用性的一種體現。
Grails 插件可服務於同樣的目的,不過,規模更大。它們可以包括很多 JAR 及 GroovyServer Page (GSP)、控制器、TagLib、服務等。就像 SiteMesh 將兩個 GSP 合並成一個一樣,插件可以將兩個或多個 Grails 應用程序合並成一個。這樣您就可以將更多的精力用於核心業務需求,在需要的時候,從外部資 源加入所需的額外功能 — 查詢、認證、備用表示層等。
此外,插件實質上也是外部 資源。雖然 Grails 開發團隊已經編寫了一些有價值的插件,但絕大多數 插件仍來自於社區。實際上,Grails 團隊一直致力於在適當的時候將其核心功能整合進插件,這就使得 Grails 自身在每次發布的時候都更小也更為穩定。
那麼如何將其應用到 Blogito — 您在本系列中逐步構建的這個 “小型博客” 應用程序中呢?假設 您想添加的下一個功能是本地搜索功能。並且您願意采用一個現有的解決方案而不是從頭構建一個您自已 的搜索基礎架構,那麼請往下看。
安裝一個搜索插件
這個搜索插件能為您的應用程序帶來類似 Google 那樣的搜索能力。它使用 Apache Lucene 創建索引 ,用 Compass 將索引鉤掛到 Grails Object Relational Mapping (GORM)/Hibernate 生命周期。這就意 味著每當您創建、更新或刪除一個 domain 類時,Lucene 索引都會相應更新。
要想安裝此插件,請鍵入 grails install-plugin searchable。(下一章節將會深入介紹安裝插件時 的技術細節。)
接下來,將這行代碼 — static searchable = true — 添加到 grails-app/domain/Entry.groovy, 如清單 2 所示:
清單 2. 讓 Entry 類成為可查詢的
class Entry {
static searchable = true
static constraints = {
title()
summary(maxSize:1000)
filename(blank:true, nullable:true)
dateCreated()
lastUpdated()
}
static mapping = {
sort "lastUpdated":"desc"
}
static belongsTo = [author:User]
String title
String summary
String filename
Date dateCreated
Date lastUpdated
}
請注意:必須要顯式地讓 domain 類變成可搜索的。這意味著您可以繼續將基礎架構數據,比如登錄 和密碼,保存在隱藏的 User 類中。
有了這一行代碼,就為 Blogito 賦予了 Lucene 和 Compass 的強大功能。鍵入 grails run-app,啟 動這個應用程序,然後訪問 http://localhost:9090/blogito/searchable。鍵入一個搜索關鍵詞,比如 grails,看一下搜索結果,如圖 1 所示:
圖 1. 默認的搜索結果
雖然搜索出一些結果,但結果不容易描述。要解決這個問題,可以為 Entry.groovy 添加一個 toString() 方法,如清單 3 所示:
清單 3. 為 Entry 添加一個 toString()
class Entry {
static searchable = true
//snip
String toString(){
"${title} (${lastUpdated})"
}
}
再次搜索 grails 。這次的結果的用戶友好性會有所提高,如圖 2 所示:
圖 2. 用 toString() 方法得到的搜索結果
這個可搜索插件的原始功能已經就緒,現在可以采取下一個步驟了:將它深入地集成到您的應用程序 內。
探索這個插件的基礎架構
縱覽 Blogito 的所有目錄,這裡似乎沒有任何新的文件。如果通過 Web 浏覽器訪問 http://localhost:9090/blogito/searchable,那裡應該會有一個 grails- app/controllers/SearchableController.groovy 文件。但奇怪的是,該文件不在那裡。在 lib 目錄中 也應該有一些 Lucene 與 Compass 的 JAR 文件,但它一如您首次鍵入 grails create-app 啟動這個項 目時一樣,是空的。實際上,對 Blogito 的惟一更改就是在 application.properties 中加入的這一行 新代碼,如清單 4 所示:
清單 4. application.properties,顯示了新安裝的 Searchable 插件
#utf-8
#Wed Jun 24 15:41:16 MDT 2009
app.version=0.4
app.servlet.version=2.4
app.grails.version=1.1.1
plugins.searchable=0.5.5
plugins.hibernate=1.1.1
app.name=blogito
通過 plug-ins.searchable 這一行代碼,可以判斷 Blogito 已經知曉 Searchable 插件的存在。那 麼所有這些功能都藏在哪了?要想查明,需返回到第一次安裝此插件時一閃而過的那個屏幕輸出。接下來 ,我將帶您探個究竟。
當鍵入 grails install-plugin searchable 後,所發生的第一件事情是向 Web 發出一個請求來拉出 插件的最新列表,如清單 5 所示:
清單 5. 下載插件的主列表
$ grails install-plugin searchable
//snip
Reading remote plugin list ...
[get] Getting: http://svn.codehaus.org/grails/trunk/grails-plugins/
.plugin-meta/plugins-list.xml
[get] To: /Users/sdavis/.grails/1.1.1/plugins-list-core.xml
[get] last modified = Mon Jun 22 04:16:31 MDT 2009
Reading remote plugin list ...
[get] Getting: http://plugins.grails.org/.plugin-meta/plugins-list.xml
[get] To: /Users/sdavis/.grails/1.1.1/plugins-list-default.xml
[get] last modified = Wed Jun 24 06:51:24 MDT 2009
這兩個列表 — core 和 default — 提供了這些插件的元數據,包括作者、描述和版本號 。更重要的是,在這裡,Grails 可以發現實際包含這些插件的 ZIP 文件所對應的 URL。清單 6 顯示了 來自於 plugins-list-core.xml 文件的有關 Hibernate 插件的信息:
清單 6. Hibernate 插件 的描述
<plugins revision="9011">
<plugin latest-release="1.1.1" name="hibernate">
<release tag="RELEASE_1_1" type="svn" version="1.1">
<title>Hibernate for Grails</title>
<author>Graeme Rocher</author>
<authorEmail/>
<description>A plugin that provides integration between
Grails and Hibernate through GORM</description>
<documentation>http://grails.org/doc/$version</documentation>
<file>http://svn.codehaus.org/grails/trunk/grails-plugins/
grails- hibernate/tags/RELEASE_1_1/grails-hibernate-1.1.zip
</file>
</release>
<!-- snip -->
</plugin>
</plugins>
目前,Hibernate 插件是核心插件文件內所列的惟一一個插件。這個列表包含了必需 插件 — Grails 運行所不能或缺的功能。默認列表包括了來自於社區的可選插件。
您是否 注意到 清單 5 中這些文件保存的位置?在主目錄中(在類似 UNIX® 的系統上,主目錄為 /Users/ 任何人;在 Windows® 系統上,主目錄為 C:\Documents and Settings\任何人)創建了一個 .grails 目錄。這個目錄內保存了在鍵入 grails run-app 時被編譯的那些類。當鍵入 grails clean 時 ,projects 下的 application 目錄會被刪除。但是,如您所見,.grails 也是存放下載插件的地方。用 文件編輯器打開 .grails/1.1.1/plugins-list-default.xml 並找到 Searchable 插件這一項。請見清單 7:
清單 7. Searchable 插件的描述
<plugin latest-release="0.5.5" name="searchable">
<release tag="RELEASE_0_5_5" type="svn" version="0.5.5">
<title>Adds rich search functionality to Grails domain models.
This version is recommended for JDK 1.5+</title>
<author>Maurice Nicholson</author>
<authorEmail>[email protected]</authorEmail>
<description>Adds rich search functionality to Grails domain models.
Built on Compass (http://www.compass-project.org/) and Lucene
(http://lucene.apache.org/)
This version is recommended for JDK 1.5+
</description>
<documentation>http://grails.org/Searchable+Plugin</documentation>
<file>http://plugins.grails.org/grails-searchable/
tags/RELEASE_0_5_5/grails-searchable-0.5.5.zip</file>
</release>
<!-- snip -->
</plugin>
一旦 Grails 發現了從哪裡可以下載這些插件,它(理所當然)會將這些所需要的插件下載到 .grails/1.1.1/plugins,如清單 8 所示:
清單 8. 下載插件
$ grails install-plugin searchable
//download core and default plugin lists
// continued...
[get] Getting: http://plugins.grails.org/grails-searchable/
tags/RELEASE_0_5_5/grails-searchable-0.5.5.zip
[get] To: /Users/sdavis/.grails/1.1.1/plugins/grails-searchable-0.5.5.zip
[get] last modified = Thu Jun 18 22:24:45 MDT 2009
最後,Grails 會從本地緩存中將這些插件復制到您的項目並進行解壓縮,如清單 9 所示:
清單 9. 向您的項目中添加插件
$ grails install-plugin searchable
//download core and default plugin lists
//download requested plugin
// continued...
[copy] Copying 1 file to /Users/sdavis/.grails/1.1.1/projects/blogito/plugins
Installing plug-in searchable-0.5.5
[mkdir] Created dir:
/Users/sdavis/.grails/1.1.1/projects/blogito/plugins/searchable-0.5.5
[unzip] Expanding:
/Users/sdavis/.grails/1.1.1/plugins/grails-searchable-0.5.5.zip into
/Users/sdavis/.grails/1.1.1/projects/blogito/plugins/searchable-0.5.5
>
更深一步研究
從 Grails 1.1 的發布說明中可以找到關於此插件基礎架構的更多信息。從中可以了解如何安裝全局 性的插件(以便所創建的每個新的項目都會自動包含這些特定的插件)、如何向列表中添加替代插件存儲 庫、如何限制插件只在特定環境中運行或是只針對特定的 Grails 命令行腳本運行,等等。
不過,在您進行太過深入的研究之前,請務必確保這對您來說具有實際意義。在 application.properties 內的行對應於 .grails 內的 project 目錄中的解壓縮目錄。這就意味著要想 卸載一個插件,可以鍵入 grails uninstall-plugin myplugin ,或者干脆將這一行從 application.properties 中刪除並手動地從 .grails 的 project 目錄中將這個目錄刪除。
插件以簡單的 ZIP 文件來回傳遞,知曉這一點非常重要。在下一篇文章中,我將向您展示如何創建您 自已的插件並通過一個本地 ZIP 文件(grails install-plugin myplugin /local/path/to/myplugin.zip)來安裝這個插件。您甚至可以通過一個遠程 URL — grails install- plugin myplugin http://somewhere.com/myplugin.zip 來安裝這個插件。
對 Searchable 插件的探討
知道了 Searchable 插件安裝的位置(.grails/1.1.1/projects/blogito/plugins/searchable-0.5.5 )後,我們就可以對它進行探討了。這個目錄結構(如圖 3 所示)應該看上去有點眼熟 — 插件和應用 程序共享同樣的基礎布局:
圖 3. 目錄結構
SearchableController 恰恰處於我們想要的位置:grails-app/controllers。在一個文件編輯器中打 開這個文件。清單 10 顯示了部分源代碼:
清單 10. SearchableController
import org.compass.core.engine.SearchEngineQueryParseException
class SearchableController {
def searchableService
def index = {
if (!params.q?.trim()) {
return [:]
}
try {
return [searchResult: searchableService.search(params.q, params)]
} catch (SearchEngineQueryParseException ex) {
return [parseException: true]
}
}
//snip
}
如您所見,SearchableService 在類被聲明後被注入到此控制器。這個熟悉的 index 動作就是默認的 目標。如果沒有傳遞進 q 參數,就會將一個空的 hashmap 返回給 grails- app/views/searchable/index.gsp。基於視圖中的邏輯,它將顯示一個空白頁。
在 index.gsp 的第 100 行左右的位置,應該能夠找到一個表單,它可設置 q 參數及遞歸地將自身提 交回 index 動作。清單 11 顯示了這個表單:
清單 11. index.gsp 中的 searchable 表單
<g:form url='[controller: "searchable", action: "index"]'
id="searchableForm"
name="searchableForm"
method="get">
<g:textField name="q" value="${params.q}" size="50"/>
<input type="submit" value="Search" />
</g:form>
回過頭,再看看 清單 10,可以發現一旦 q 參數內有了一個搜索條件,searchableService.search() 調用的結果就會被返回給 index.gsp。在 index.gsp 中的第 150 行左右,會顯示這些結果,如清單 12 所示:
清單 12. 顯示搜索結果
<g:if test="${haveResults}">
<div class="results">
<g:each var="result" in="${searchResult.results}" status="index">
<div class="result">
<g:set var="className" value="${ClassUtils.getShortName(result.getClass())}" />
<g:set var="link"
value="${createLink(controller: className[0].toLowerCase() +
className[1..-1],
action: 'show',
id: result.id)}" />
<div class="name"><a href="${link}">${className} #${result.id} </a></div>
<g:set var="desc" value="${result.toString()}" />
<g:if test="${desc.size() > 120}">
<g:set var="desc" value="${desc[0..120] + '...'}" />
</g:if>
<div class="desc">${desc.encodeAsHTML()}</div>
<div class="displayLink">${link}</div>
</div>
</g:each>
</div>
<!-- snip -->
</g:if>
我鼓勵您更深入地去探索 Searchable 插件的奧秘。請見 grails- app/services/SearchableService.groovy。注意到 lib 目錄中已經包含了 Lucene 和 Compass 的 JAR 文件。到 src/java 和 src/groovy 目錄去看看所有支持的類。再回顧一下 tests 目錄中的 GroovyTestCase。一個典型 Grails 應用程序的所有部分都在這個插件裡。
每當安裝一個新插件,都要留意一下它的實現。這將有助於您識別所有可移動部分、了解它們是如何 組合起來發揮作用的,並且 — 最重要的是 — 給您啟示,教您如何能更好地將它們融入到您的應用程序 中。接下來的一節,您將看到如何將搜索功能從默認實現轉到您自已的定制組件中。
將搜索更深入地並入到 Blogito
下面教您如何添加對 Entries 的搜索。首先,在一個文本編輯器內打開 grails- app/controllers/EntryController.groovy。添加一個簡單的 search 動作,如清單 13 所示。(別忘了 要允許未經身份驗證的用戶通過向 beforeInterceptor 添加 search 動作來進行博客條目的搜索。)
清單 13. 添加 search 動作
class EntryController {
def beforeInterceptor =
[action:this.&auth, except:["index", "list", "show", "atom", "search"]]
def search = {
render Entry.search(params.q, params)
}
//snip
}
正如在前一章節所展示的那樣,SearchableService 非常適合用來進行跨所有域類的站點級別的搜索 。但 Searchable 插件也可以在您個人的域類上做一些元編程。正像 Grails 可以動態地添加 list()、 get() 和 findBy() 方法一樣,Searchable 插件可以添加一個 search() 方法。
通過在 Web 浏覽器中鍵入 http://localhost:9090/blogito/entry/search?q=groovy 來測試新的 search 動作。應該會看到一個搜索結果的 hashmap 圖,類似於圖 4:
圖 4. 顯示原始的搜索結果
知道了 search() 方法的工作原理後,下一步是要讓用戶界面更為友好一點。在 grails- app/views/layouts 中創建一個名為 _search.gsp 的局部模板。加入清單 14 中的代碼:
清單 14. 局部模板
<div id="search">
<g:form url='[controller: "entry", action: "search"]'
id="searchableForm"
name="searchableForm"
method="get">
<g:textField name="q" value="${params.q}" />
<input type="submit" value="Search" />
</g:form>
</div>
請注意,在上述代碼中,控制器被設為 entry,動作被設為 search。
接下來,該顯示這個局部模板了。在一個文本編輯器內打開 grails-app/views/layouts/_header.gsp 並添加一個 render 標簽,如清單 15 所示:
清單 15. 為 header 添加這個搜索模板
<g:render template="/layouts/search" />
<div id="header">
<p><g:link class="header-main" controller="entry">Blogito</g:link></p>
<p class="header-sub">
<g:link controller="entry" action="atom">
<img src="${createLinkTo(dir:'images',file:'feed-icon-28x28.png')}"
alt="Subscribe" title="Subscribe"/>
</g:link>
A tiny little blog
</p>
<div id="loginHeader">
<g:loginControl />
</div>
</div>
給 web-app/css/main.css 添加一些 Cascading Style Sheets (CSS) 以確保 search <div> 可以浮在屏幕的右上角,如清單 16 所示:
清單 16. 添加 CSS 來調整搜索表單的位置
#search {
float: right;
margin: 2em 1em;
}
所有視圖變化均完成後,請刷新浏覽器。屏幕看上去應該如圖 5 所示:
圖 5. 給 header 添加搜索表單
需要做的最後一件事情就是以 HTML 格式提交 search 結果,而不是簡單的調試輸出。調整 EntryController 內的 search 動作,如清單 17 所示:
清單 17. 一個更健壯的搜索動作
def search = {
//render Entry.search(params.q, params)
def searchResults = Entry.search(params.q, params)
flash.message = "${searchResults.total} results found for search: ${params.q}"
flash.q = params.q
return [searchResults:searchResults.results, resultCount:searchResults.total]
}
由於該動作被命名為 search,因此需要在 grails-app/views/entry 中創建對應的 search.gsp 文件 ,如清單 18 所示:
清單 18. Search.gsp
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main" />
<title>Blogito</title>
</head>
<body>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<div class="body">
<div class="list">
<g:each in="${searchResults}" status="i" var="entry">
<div class="entry">
<h2>
<g:link action="show"
id="${entry.id}">${entry.title}</g:link>
</h2>
<p>${entry.summary}</p>
</div>
</g:each>
</div>
</div>
<div class="paginateButtons">
<g:paginate total="${resultCount}" params="${flash}"/>
</div>
</body>
</html>
在 Web 浏覽器中最後做一次 grails 搜索。搜索結果應該如圖 6 所示:
圖 6. HTML 格式的搜索結果
結束語
插件是 Grails 體系中最令人興奮、最為活躍的一部分。它們可以讓您坐享各式各樣的現成功能。一 旦您掌握了自己的代碼庫(application.properties 和 .grails 目錄)的觸點所在,您就可以研究源代 碼以更好地理解插件作者是如何實現魔法的,同時還能為如何與您自已的代碼進行深入集成找到靈感。
下一次,我將向您展示如何創建一個自已的插件。到那時,請享受精通 Grails 的樂趣吧!