感谢作者文章,转自:http://www.ibm.com/developerworks/cn/java/j-grails07219/
在这一期的 精通 Grails 中,Scott Davis 将向您展示 Grails 插件的精彩世界。向应用程序中添加新功能将是一件再简单不过的事情。您将领略插件是如何具有如此魔力的,您还会在 Blogito 应用程序中使用一个插件来实现强大的搜索功能。
|
在开始阶段,精通 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 所示:
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 所示:
虽然搜索出一些结果,但结果不容易描述。要解决这个问题,可以为 Entry.groovy 添加一个 toString()
方法,如清单 3 所示:
class Entry { static searchable = true //snip String toString(){ "${title} (${lastUpdated})" } } |
再次搜索 grails
。这次的结果的用户友好性会有所提高,如图 2 所示:
这个可搜索插件的原始功能已经就绪,现在可以采取下一个步骤了:将它深入地集成到您的应用程序内。
|
纵览 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 所示:
$ 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 插件的信息:
<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:
<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>maurice@freeshell.org</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 所示:
$ 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 所示:
$ 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 |
>
|
不过,在您进行太过深入的研究之前,请务必确保这对您来说具有实际意义。在 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 插件安装的位置(.grails/1.1.1/projects/blogito/plugins/searchable-0.5.5)后,我们就可以对它进行探讨了。这个目录结构(如图 3 所示)应该看上去有点眼熟 — 插件和应用程序共享同样的基础布局:
SearchableController
恰恰处于我们想要的位置:grails-app/controllers。在一个文件编辑器中打开这个文件。清单 10 显示了部分源代码:
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 所示:
<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 应用程序的所有部分都在这个插件里。
每当安装一个新插件,都要留意一下它的实现。这将有助于您识别所有可移动部分、了解它们是如何组合起来发挥作用的,并且 — 最重要的是 — 给您启示,教您如何能更好地将它们融入到您的应用程序中。接下来的一节,您将看到如何将搜索功能从默认实现转到您自已的定制组件中。
|
下面教您如何添加对 Entries
的搜索。首先,在一个文本编辑器内打开 grails-app/controllers/EntryController.groovy。添加一个简单的 search
动作,如清单 13 所示。(别忘了要允许未经身份验证的用户通过向 beforeInterceptor
添加 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:
知道了 search()
方法的工作原理后,下一步是要让用户界面更为友好一点。在 grails-app/views/layouts 中创建一个名为 _search.gsp 的局部模板。加入清单 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 所示:
<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 所示:
#search { float: right; margin: 2em 1em; } |
所有视图变化均完成后,请刷新浏览器。屏幕看上去应该如图 5 所示:
需要做的最后一件事情就是以 HTML 格式提交 search
结果,而不是简单的调试输出。调整 EntryController
内的 search
动作,如清单 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 所示:
<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 所示:
|
插件是 Grails 体系中最令人兴奋、最为活跃的一部分。它们可以让您坐享各式各样的现成功能。一旦您掌握了自己的代码库(application.properties 和 .grails 目录)的触点所在,您就可以研究源代码以更好地理解插件作者是如何实现魔法的,同时还能为如何与您自已的代码进行深入集成找到灵感。
下一次,我将向您展示如何创建一个自已的插件。到那时,请享受精通 Grails 的乐趣吧!