对于事件驱动的反应性开发,构建 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
目标
|
可见,它的内容并不多。首先运行
clean
目标,然后运行
cleanTestReports
目标。调用堆栈后,看一下
clean
目标,如清单 2 所示:
清单 2. Clean.groovy 中的
clean
目标
|
如果需要自定义
clean
命令的行为,可以在此添加自己的代码。不过,使用此方法的问题是:每次升级 Grails 时都必须迁移自定义内容。而且从一台计算机移动到另一台计算机时,您的构建会更容易出错。(Grails 安装文件很少签入版本控制 — 只检签入用程序代码)。为了避免可怕的 “but it works on my box” 综合症,我倾向于将这些类型的自定义内容放在项目中。这确保来自源控件的所有新签出都包含成功构建所需的自定义内容。如果使用持续集成服务器(比如 CruiseControl),也有助于保持一致性。
注意,在
clean
目标期间会抛出几个事件。
CleanStart
在过程开始之前发生,随后发生
CleanEnd
。您可以在项目中引入这些事件,将自定义代码与项目放在一起,不要改动 Grails 安装文件。您只需要创建一个监听器。
在项目的脚本目录中创建一个名为 Events.groovy 的文件。添加清单 3 所示的代码:
清单 3. 向 Events.groovy 添加事件监听器
|
如果输入
grails clean
,应该看到类似于清单 4 的输出:
清单 4. 显示新注释的控制台输出
|
当然,您可以不向控制台写入简单的消息,而是进行一些实际工作。可能需要删除一些额外的目录。您可能喜欢通过用新的文件覆盖现有文件来 “重置” XML 文件。任何能在 Groovy(或通过 Java 编程)中完成的工作都可以在这里完成。
CreateFile
事件
以下是另一个可在构建期间引入的事件示例。每次输入
create-
命令之一(
create-controller
、
create-domain-class
等等),都会触发
CreatedFile
事件。看看 scripts/CreateDomainClass.groovy,如清单 5 所示:
清单 5. CreateDomainClass.groovy
|
在此不能看到
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 添加工件
|
现在输入
grails create-domain-class Hotel
并查看结果。如果没有使用 Subversion,此命令将静默失败。如果使用 Subversion,输入
svn status
。此时应该看到添加的文件(域类和对应的集成测试)。
发现调用的构建事件
要发现什么脚本抛出什么事件,最快方式是搜索 Grails 脚本中的
event()
调用。在 UNIX® 系统中,可以使用
grep
搜索 Groovy 脚本中的
event
字符串,如清单 7 所示:
清单 7. 使用
Grep
搜索 Grails 脚本中的事件调用
|
知道调用的事件后,可以在 scripts/Events.groovy 中创建相应的监听器,并高度自定义构建环境。
抛出自定义事件
显然,现在已经了解相关的原理,您可以随意添加自己的事件了。如果确实需要自定义 $GRAILS_HOME/scripts 中的脚本(我们随后将进行此操作以抛出自定义事件),我建议将它们复制到项目内的脚本目录中。这意味着自定义脚本会和其他内容一起签入到源控件中。Grails 询问运行哪个版本的脚本 — $GRAILS_HOME 或本地脚本目录中的脚本。
将 $GRAILS_HOME/scripts/Clean.groovy 复制到本地脚本目录,并在
CleanEnd
事件后添加以下事件:
|
第一个参数是事件的名称,第二个参数是要返回的项目列表。在本例中,返回一个当前日期戳和一条自定义消息。
将清单 8 中的闭包添加到 scripts/Events.groovy:
清单 8. 捕获自定义事件
|
输入
grails clean
并选择本地脚本版本后,应该看到如下内容:
|
启动
除了构建事件,还可以引入应用程序事件。在每次启动和停止 Grails 时会运行 grails-app/conf/BootStrap.groovy 文件。在文本编辑器中打开 BootStrap.groovy。
init
闭包在启动时调用。
destroy
闭包在应用程序关闭时调用。
首先,向闭包添加一些简单文本,如清单 9 所示:
清单 9. 以 BootStrap.groovy 开始
|
输入
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
类添加一个字段
|
现在构建一个
HotelController
,如清单 11 所示:
清单 11. 创建一个 Hotel Controller
|
注意:如果像 “Grails 与遗留数据库” 中讨论的那样禁用 grails-app/conf/DataSource.groovy 中的
dbCreate
变量,本例则应该重新添加它并设置为
update
。当然,还有另一种选择是通过手动方式让
Hotel
表与
Hotel
类的更改保持一致。
现在将清单 12 中的代码添加到 BootStrap.groovy:
清单 12. 保存和删除 BootStrap.groovy 中的记录
|
在接下来的几个示例中,需要一直打开 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. 防故障插入和删除
|
如果调用
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 中特定于环境的行为
|
现在具备只在测试模式下插入记录的条件。但不要在此停住。我通常在 XML 文件中外部化测试数据。将这里所学到的知识与 “Grails 与遗留数据库” 中的 XML 备份和还原脚本相结合,就会得到了一个功能强大的测试平台(testbed)。
因为 BootStrap.groovy 是一个可执行的脚本,而不是被动配置文件,所以理论上可以在 Groovy 中做任何事情。您可能需要在启动时调用一个 Web 服务,通知中央服务器该实例正在运行。或者需要同步来自公共源的本地查找表。这一切都有可能实现。
微型事件
了解一些大型事件后,现在看几个微型事件。
为域类添加时间戳
如果您提供几个特别的命名字段,GORM 会自动给它们添加时间戳,如清单 15 所示:
清单 15. 为字段添加时间戳
|
顾名思义,
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 中删除时间戳字段
|
这会限制在创建和编辑视图中创建字段,但仍然在列表中保留字段并显示视图。创建一两个
Hotel
并验证字段会自动更新。
如果应用程序已经使用这些字段名称,可以轻松地禁用此功能,如清单 17 所示:
清单 17. 禁用时间戳
|
回忆一下 “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. 映射时间戳字段
|
另一种方案是将域类中的字段命名为与企业列名称匹配的名称,并创建
beforeInsert
和
beforeUpdate
闭包来填充字段,如清单 19 所示(不要忘记将新字段设置为
nullable
— 否则
save()
方法会在 BootStrap.groovy 中静默失败)。
清单 19. 添加
beforeInsert
和
beforeUpdate
闭包
|
启动和停止应用程序几次,确保新字段按预期填充。
像到目前为止看到的所有其他事件一样,您可以决定如何使用它们。回忆一下 “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。