Grails 中的所有内容,从构建脚本到单个工件(比如域类和控制器),都会在应用程序生命周期的关键点抛出事件。在这篇精通 Grails 文章中,您将学习如何设置监听器来捕获这些事件,并且通过自定义行为做出反应。
对于事件驱动的反应性开发,构建 Web 站点是一门学问。您的应用程序是不是很空闲,焦虑地等待用户发送请求,然后它传回响应,再返回休眠状态,直到下次调用。除了传统的 Web 生命周期的 HTTP 请求和响应,Grails 还提供了大量自定义接触点,您可以在此进入事件模型并提供自己的行为。
|
在本文中,您将发现构建过程中会抛出很多事件。需要自定义地启动和关闭应用程序。最后,探讨 Grails 域类的生命周期事件。
构建事件
开发 Grails 的第一步是输入 grails create-app。最后输入 grails run-app 或 grails war。这期间输入的所有命令和内容都会在过程的关键点抛出事件。
查看 $GRAILS_HOME/scripts 目录。此目录中的文件是 Gant 脚本,对应输入的命令。例如,输入 grails clean 时,调用 Clean.groovy。
|
在文本编辑器中打开 Clean.groovy。首先看到的目标是 default 目标,如清单 1 所示:
清单 1. Clean.groovy 中的 default 目标
target ('default': "Cleans a Grails project") { clean() cleanTestReports() } |
可见,它的内容并不多。首先运行 clean 目标,然后运行 cleanTestReports 目标。调用堆栈后,看一下 clean 目标,如清单 2 所示:
清单 2. Clean.groovy 中的 clean 目标
target ( clean: "Implementation of clean") { event("CleanStart", []) depends(cleanCompiledSources, cleanGrailsApp, cleanWarFile) event("CleanEnd", []) } |
如果需要自定义 clean 命令的行为,可以在此添加自己的代码。不过,使用此方法的问题是:每次升级 Grails 时都必须迁移自定义内容。而且从一台计算机移动到另一台计算机时,您的构建会更容易出错。(Grails 安装文件很少签入版本控制 — 只检签入用程序代码)。为了避免可怕的 “but it works on my box” 综合症,我倾向于将这些类型的自定义内容放在项目中。这确保来自源控件的所有新签出都包含成功构建所需的自定义内容。如果使用持续集成服务器(比如 CruiseControl),也有助于保持一致性。
注意,在 clean 目标期间会抛出几个事件。CleanStart 在过程开始之前发生,随后发生 CleanEnd。您可以在项目中引入这些事件,将自定义代码与项目放在一起,不要改动 Grails 安装文件。您只需要创建一个监听器。
在项目的脚本目录中创建一个名为 Events.groovy 的文件。添加清单 3 所示的代码:
清单 3. 向 Events.groovy 添加事件监听器
eventCleanStart = { println "### About to clean" } eventCleanEnd = { println "### Cleaning complete" } |
如果输入 grails clean,应该看到类似于清单 4 的输出:
清单 4. 显示新注释的控制台输出
$ grails clean Welcome to Grails 1.0.3 - http://grails.org/ Licensed under Apache Standard License 2.0 Grails home is set to: /opt/grails Base Directory: /src/trip-planner2 Note: No plugin scripts found Running script /opt/grails/scripts/Clean.groovy Environment set to development Found application events script ### About to clean [delete] Deleting: /Users/sdavis/.grails/1.0.3/projects/trip-planner2/resources/web.xml [delete] Deleting directory /Users/sdavis/.grails/1.0.3/projects/trip-planner2/classes [delete] Deleting directory /Users/sdavis/.grails/1.0.3/projects/trip-planner2/resources ### Cleaning complete |
当然,您可以不向控制台写入简单的消息,而是进行一些实际工作。可能需要删除一些额外的目录。您可能喜欢通过用新的文件覆盖现有文件来 “重置” XML 文件。任何能在 Groovy(或通过 Java 编程)中完成的工作都可以在这里完成。
CreateFile 事件
以下是另一个可在构建期间引入的事件示例。每次输入 create- 命令之一(create-controller、create-domain-class 等等),都会触发 CreatedFile 事件。看看 scripts/CreateDomainClass.groovy,如清单 5 所示:
清单 5. CreateDomainClass.groovy
Ant.property(environment:"env") grailsHome = Ant.antProject.properties."env.GRAILS_HOME" includeTargets << new File ( "${grailsHome}/scripts/Init.groovy" ) includeTargets << new File( "${grailsHome}/scripts/CreateIntegrationTest.groovy") target ('default': "Creates a new domain class") { depends(checkVersion) typeName = "" artifactName = "DomainClass" artifactPath = "grails-app/domain" createArtifact() createTestSuite() } |
在此不能看到 CreatedFile 事件的调用,不过看一下 $GRAILS_HOME/scripts/Init.groovy 中的 createArtifact 目标($GRAILS_HOME/scripts/CreateIntegrationTest.groovy 中的 createTestSuite 目标最终也调用 $GRAILS_HOME/scripts/Init.groovy 中的 createArtifact 目标)。在 createArtifact 目标的倒数第二行,可以看到以下调用:event("CreatedFile", [artifactFile])。
该事件与 CleanStart 事件的最大差异是:前者会将一个值传回给事件处理程序。在本例中,它是刚才创建的文件的完全路径(随后会看到,第二个参数是一个列表 — 可以需要传递回以逗号分隔的值)。必须设置事件处理程序来捕获传入的值。
假设您想将这些新创建的文件自动添加到源控件。在 Groovy 中,可以将平时在命令行中输入的所有内容包含在引号内并在 String 上调用 execute()。将清单 6 中的事件处理程序添加到 scripts/Events.groovy:
清单 6. 自动向 Subversion 添加工件
eventCreatedFile = {fileName -> "svn add ${fileName}".execute() println "### ${fileName} was just added to Subversion." } |
现在输入 grails create-domain-class Hotel 并查看结果。如果没有使用 Subversion,此命令将静默失败。如果使用 Subversion,输入 svn status。此时应该看到添加的文件(域类和对应的集成测试)。
发现调用的构建事件
要发现什么脚本抛出什么事件,最快方式是搜索 Grails 脚本中的 event() 调用。在 UNIX® 系统中,可以使用 grep 搜索 Groovy 脚本中的 event 字符串,如清单 7 所示:
清单 7. 使用 Grep 搜索 Grails 脚本中的事件调用
$ grep "event(" *.groovy Bootstrap.groovy: event("AppLoadStart", ["Loading Grails Application"]) Bootstrap.groovy: event("AppLoadEnd", ["Loading Grails Application"]) Bootstrap.groovy: event("ConfigureAppStart", [grailsApp, appCtx]) Bootstrap.groovy: event("ConfigureAppEnd", [grailsApp, appCtx]) BugReport.groovy: event("StatusFinal", ["Created bug-report ZIP at ${zipName}"]) |
知道调用的事件后,可以在 scripts/Events.groovy 中创建相应的监听器,并高度自定义构建环境。
抛出自定义事件
显然,现在已经了解相关的原理,您可以随意添加自己的事件了。如果确实需要自定义 $GRAILS_HOME/scripts 中的脚本(我们随后将进行此操作以抛出自定义事件),我建议将它们复制到项目内的脚本目录中。这意味着自定义脚本会和其他内容一起签入到源控件中。Grails 询问运行哪个版本的脚本 — $GRAILS_HOME 或本地脚本目录中的脚本。
将 $GRAILS_HOME/scripts/Clean.groovy 复制到本地脚本目录,并在 CleanEnd 事件后添加以下事件:
event("TestEvent", [new Date(), "Some Custom Value"]) |
第一个参数是事件的名称,第二个参数是要返回的项目列表。在本例中,返回一个当前日期戳和一条自定义消息。
将清单 8 中的闭包添加到 scripts/Events.groovy:
清单 8. 捕获自定义事件
eventTestEvent = {timestamp, msg -> println "### ${msg} occurred at ${timestamp}" } |
输入 grails clean 并选择本地脚本版本后,应该看到如下内容:
### Some Custom Value occurred at Wed Jul 09 08:27:04 MDT 2008 |
。
启动
除了构建事件,还可以引入应用程序事件。在每次启动和停止 Grails 时会运行 grails-app/conf/BootStrap.groovy 文件。在文本编辑器中打开 BootStrap.groovy。init 闭包在启动时调用。destroy 闭包在应用程序关闭时调用。
首先,向闭包添加一些简单文本,如清单 9 所示:
清单 9. 以 BootStrap.groovy 开始
def init = { println "### Starting up" } def destroy = { println "### Shutting down" } |
输入 grails run-app 启动应用程序。应该会程序末尾附近看到 ### Starting Up 消息。
现在按 CTRL+C。看到 ### Shutting Down 消息了吗?我没有看到。问题在于 CTRL+C 会突然停止服务器,而不调用 destroy 闭包。Rest 确保在应用服务器关闭时会调用此闭包。但无需输入 grails war 并在 Tomcat 或 IBM®WebSphere® 中加载 WAR 来查看 destroy 事件。
要查看 init 和 destroy 事件触发,输入 grails interactive 以交互模式启动 Grails。现在输入 run-app 启动应用程序,输入 exit 关闭服务器。以交互模式运行会大大加快开发过程,因为 JVM 一直在运行并随时可用。其中一个优点是,与使用 CTRL+C 强硬方法相比,应用程序关闭得更恰当。
在启动期间向数据库添加记录
使用 BootStrap.groovy 脚本除了提供简单的控制台输出,还能做什么呢?通常,人们使用这些挂钩将记录插入数据库中。
首先,向先前创建的 Hotel 类中添加一个名称字段,如清单 10 所示:
清单 10. 向 Hotel 类添加一个字段
class Hotel{ String name } |
现在构建一个 HotelController,如清单 11 所示:
清单 11. 创建一个 Hotel Controller
class HotelController { def scaffold = Hotel } |
注意:如果像 “Grails 与遗留数据库” 中讨论的那样禁用 grails-app/conf/DataSource.groovy 中的 dbCreate 变量,本例则应该重新添加它并设置为 update。当然,还有另一种选择是通过手动方式让 Hotel 表与 Hotel 类的更改保持一致。
现在将清单 12 中的代码添加到 BootStrap.groovy:
清单 12. 保存和删除 BootStrap.groovy 中的记录
def init = { servletContext -> new Hotel(name:"Marriott").save() new Hotel(name:"Sheraton").save() } def destroy = { Hotel.findByName("Marriott").delete() Hotel.findByName("Sheraton").delete() } |
在接下来的几个示例中,需要一直打开 MySQL 控制台并观察数据库。输入 mysql --user=grails -p --database=trip 登录(记住,密码是 server)。然后执行以下步骤:
- 如果 Grails 还没有运行就启动它。
- 输入 show tables; 确认已创建 Hotel 表。
- 输入 desc hotel; 查看列和数据类型。
- 输入 select from hotel; 确认记录已插入。
- 输入 delete from hotel; 删除所有记录。
BootStrap.groovy 中的防故障数据库插入和删除
在 BootStrap.groovy 中执行数据库插入和删除操作时可能需要一定的防故障措施。如果在插入之前没有检查记录是否存在,可能会在数据库中得到重复项。如果试着删除不存在的记录,会看到在控制台上抛出恶意异常。清单 13 说明了如何执行防故障插入和删除:
清单 13. 防故障插入和删除
def init = { servletContext -> def hotel = Hotel.findByName("Marriott") if(!hotel){ new Hotel(name:"Marriott").save() } hotel = Hotel.findByName("Sheraton") if(!hotel){ new Hotel(name:"Sheraton").save() } } def destroy = { def hotel = Hotel.findByName("Marriott") if(hotel){ Hotel.findByName("Marriott").delete() } hotel = Hotel.findByName("Sheraton") if(hotel){ Hotel.findByName("Sheraton").delete() } } |
如果调用 Hotel.findByName("Marriott"),并且 Hotel 不存在表中,就会返回一个 null 对象。下一行 if(!hotel) 只有在值非空时才等于 true。这确保了只在新 Hotel 还不存在时才保存它。在 destroy 闭包中,执行相同的测试,确保不删除不存在的记录。
在 BootStrap.groovy 中执行特定于环境的行为
如果希望行为只在以特定的模式中运行时才发生,可以借助 GrailsUtil 类。在文件顶部导入 grails.util.GrailsUtil。静态 GrailsUtil.getEnvironment() 方法(由于 Groovy 的速记 getter 语法,简写为 GrailsUtil.environment)指明运行的模式。将此与 switch 语句结合起来,如清单 14 所示,可以在 Grails 启动时让特定于环境的行为发生:
|
清单 14. BootStrap.groovy 中特定于环境的行为
import grails.util.GrailsUtil class BootStrap { def init = { servletContext -> switch(GrailsUtil.environment){ case "development": println "#### Development Mode (Start Up)" break case "test": println "#### Test Mode (Start Up)" break case "production": println "#### Production Mode (Start Up)" break } } def destroy = { switch(GrailsUtil.environment){ case "development": println "#### Development Mode (Shut Down)" break case "test": println "#### Test Mode (Shut Down)" break case "production": println "#### Production Mode (Shut Down)" break } } } |
现在具备只在测试模式下插入记录的条件。但不要在此停住。我通常在 XML 文件中外部化测试数据。将这里所学到的知识与 “Grails 与遗留数据库” 中的 XML 备份和还原脚本相结合,就会得到了一个功能强大的测试平台(testbed)。
因为 BootStrap.groovy 是一个可执行的脚本,而不是被动配置文件,所以理论上可以在 Groovy 中做任何事情。您可能需要在启动时调用一个 Web 服务,通知中央服务器该实例正在运行。或者需要同步来自公共源的本地查找表。这一切都有可能实现。
微型事件
了解一些大型事件后,现在看几个微型事件。
为域类添加时间戳
如果您提供几个特别的命名字段,GORM 会自动给它们添加时间戳,如清单 15 所示:
清单 15. 为字段添加时间戳
class Hotel{ String name Date dateCreated Date lastUpdated } |
顾名思义,dateCreated 字段在数据第一次插入到数据库时被填充。lastUpdated 字段在每次数据库记录更新之后被填充。
要验证这些字段在幕后被填充,需要再做一件事:在创建和编辑视图中禁用它们。为此,可以输入 grails generate-views Hotel 并删除 create.gsp 和 edit.gsp 文件中的字段,但有一种方法使 scaffolded 视图更具动态性。在 “用 Groovy 服务器页面(GSP)改变视图” 中,您输入了 grails install-templates,以便能够调试 scaffolded 视图。查看 scripts/templates/scaffolding 中的 create.gsp 和 edit.gsp。现在向模板中的 excludedProps 列表添加两个时间戳字段,如清单 16 所示:
清单 16. 从默认 scaffolding 中删除时间戳字段
excludedProps = ['dateCreated','lastUpdated', 'version', 'id', Events.ONLOAD_EVENT, Events.BEFORE_DELETE_EVENT, Events.BEFORE_INSERT_EVENT, Events.BEFORE_UPDATE_EVENT] |
这会限制在创建和编辑视图中创建字段,但仍然在列表中保留字段并显示视图。创建一两个 Hotel 并验证字段会自动更新。
如果应用程序已经使用这些字段名称,可以轻松地禁用此功能,如清单 17 所示:
清单 17. 禁用时间戳
static mapping = { autoTimestamp false } |
回忆一下 “Grails 与遗留数据库”,在那里还可以指定 version false 来禁用 version 字段的自动创建和更新。
向域类添加事件处理程序
除了给域类添加时间戳,还可以引入 4 个事件挂钩:beforeInsert、befortUpdate、beforeDelete 和 onload。
这些闭包名称反映了它们的含义。beforeInsert 闭包在 save() 方法之前调用。beforeUpdate 闭包在 update() 方法之前调用。beforeDelete 闭包在 delete() 方法之前调用。最后,从数据库加载类后调用 onload。
假设您的公司已经制有给数据库记录加时间戳的策略,而且将这些字段的名称标准化为 cr_time 和 up_time。有几个方案可使 Grails 符合这个企业策略。一个是使用在 “Grails 与遗留数据库” 中学到的静态映射技巧将默认 Grails 字段名称与默认公司列名称关联,如清单 18 所示:
清单 18. 映射时间戳字段
class Hotel{ Date dateCreated Date lastUpdated static mapping = { columns { dateCreated column: "cr_time" lastUpdated column: "up_time" } } } |
另一种方案是将域类中的字段命名为与企业列名称匹配的名称,并创建 beforeInsert 和 beforeUpdate 闭包来填充字段,如清单 19 所示(不要忘记将新字段设置为 nullable — 否则 save() 方法会在 BootStrap.groovy 中静默失败)。
清单 19. 添加 beforeInsert 和 beforeUpdate 闭包
class Hotel{ static constraints = { name() crTime(nullable:true) upTime(nullable:true) } String name Date crTime Date upTime def beforeInsert = { crTime = new Date() } def beforeUpdate = { upTime = new Date() } } |
启动和停止应用程序几次,确保新字段按预期填充。
像到目前为止看到的所有其他事件一样,您可以决定如何使用它们。回忆一下 “Grails 服务和 Google 地图”,您创建了一个 Geocoding 服务来将街道地址转换为纬度/经度坐标,以便可以在地图上标示一个 Airport。在那篇文章中,我让您在 AirportController 中调用 save 和 update 闭包中的服务。我曾试图将此服务调用移动到 Airport 类中的 beforeInsert 和 beforeUpdate,以使它能够透明地自动发生。
如何在所有类中共享这个行为呢?我将这些字段和闭包添加到 src/templates 中的默认 DomainClass 模板中。这样,新创建域类时它们就有适当的字段和事件闭包。
结束语
Grails 中的事件能帮助您进一步自定义应用程序运行的方式。可以扩展构建过程,而无需通过在脚本目录中创建一个 Events.groovy 文件来修改标准 Grails 脚本。可以通过向 BootStrap.groovy 文件中的 init 和 destroy 闭包添加自己的代码来自定义启动和关闭进程。最后,向域类添加 beforeInsert 和 beforeUpdate 等闭包,这允许您添加时间戳和地理编码等行为。
在下一篇文章中,我将介绍使用 Grails 创建基于数据具象状态传输(Representational State Transfer,REST)的 Web 服务的思想。您将看到 Grails 能轻松支持 HTTP GET、PUT、POST 和 DELETE 操作,而它们是支持下一代 REST 式 Web 服务所需的。到那时,仍然需要精通 Grails。(责任编辑:A6)