精通 Grails: Grails 事件模型

自定义贯穿应用程序生命周期的行为

Grails 中的所有内容,从构建脚本到单个工件(比如域类和控制器),都会在应用程序生命周期的关键点抛出事件。在这篇精通 Grails 文章中,您将学习如何设置监听器来捕获这些事件,并且通过自定义行为做出反应。

Scott Davis , 主编, AboutGroovy.com

Scott Davis 是国际知名作家、演讲家和软件开发人员。他出版的书籍包括 Groovy Recipes: Greasing the Wheels of JavaGIS for Web Developers: Adding Where to Your ApplicationThe Google Maps APIJBoss At Work



2008 年 9 月 12 日

对于事件驱动的反应性开发,构建 Web 站点是一门学问。您的应用程序是不是很空闲,焦虑地等待用户发送请求,然后它传回响应,再返回休眠状态,直到下次调用。除了传统的 Web 生命周期 的 HTTP 请求和响应,Grails 还提供了大量自定义接触点,您可以在此进入事件模型并提供自己的行为。

关于本系列

Grails 是一个现代 Web 开发框架,集成了熟悉的 Java 技术(比如 Spring 和 Hibernate)和当今实践(比如配置约定)。Grails 是用 Groovy 编写的,在与传统 Java™ 代码无缝集成的同时增加了脚本语言的灵活性和动态性。学习 Grails 后,您将用另一种目光看待 Web 开发了。

在本文中,您将发现构建过程中会抛出很多事件。需要自定义地启动和关闭应用程序。最后,探讨 Grails 域类的生命周期事件。

构建事件

开发 Grails 的第一步是输入 grails create-app。最后输入 grails run-appgrails war。这期间输入的所有命令和内容都会在过程的关键点抛出事件。

查看 $GRAILS_HOME/scripts 目录。此目录中的文件是 Gant 脚本,对应输入的命令。例如,输入 grails clean 时,调用 Clean.groovy。

Gant 的 groovy 特性

您在 第一篇文章 中第一次看到了 Grant 脚本。注意,Gant 是针对 Apache Ant 设计的瘦 Groovy。Gant 没有重新实现 Ant 任务 — 它实际上调用底层 Ant 代码来实现最大的兼容性。在 Ant 中能做的一切事情也可以在 Grant 中完成。惟一的区别在于 Gant 脚本是 Groovy 脚本,而不是 XML 文件(有关 Gant 的更多信息,请参阅 参考资料)。

在文本编辑器中打开 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-controllercreate-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 事件。

要查看 initdestroy 事件触发,输入 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)。然后执行以下步骤:

  1. 如果 Grails 还没有运行就启动它。
  2. 输入 show tables; 确认已创建 Hotel 表。
  3. 输入 desc hotel; 查看列和数据类型。
  4. 输入 select from hotel; 确认记录已插入。
  5. 输入 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 启动时让特定于环境的行为发生:

Groovy 健壮的 switch

注意,Groovy 的 switch 语句比 Java switch 语句更健壮。在 Java 代码中,只能开启整数值。在 Groovy 中,还可以开启 String 值。

清单 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 个事件挂钩:beforeInsertbefortUpdatebeforeDeleteonload

这些闭包名称反映了它们的含义。beforeInsert 闭包在 save() 方法之前调用。beforeUpdate 闭包在 update() 方法之前调用。beforeDelete 闭包在 delete() 方法之前调用。最后,从数据库加载类后调用 onload

假设您的公司已经制有给数据库记录加时间戳的策略,而且将这些字段的名称标准化为 cr_timeup_time。有几个方案可使 Grails 符合这个企业策略。一个是使用在 “Grails 与遗留数据库” 中学到的静态映射技巧将默认 Grails 字段名称与默认公司列名称关联,如清单 18 所示:

清单 18. 映射时间戳字段
class Hotel{
  Date dateCreated
  Date lastUpdated
  
  static mapping = {
    columns {
      dateCreated column: "cr_time"
      lastUpdated column: "up_time"
    }
  }
}

另一种方案是将域类中的字段命名为与企业列名称匹配的名称,并创建 beforeInsertbeforeUpdate 闭包来填充字段,如清单 19 所示(不要忘记将新字段设置为 nullable— 否则 save() 方法会在 BootStrap.groovy 中静默失败)。

清单 19. 添加 beforeInsertbeforeUpdate 闭包
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 中调用 saveupdate 闭包中的服务。我曾试图将此服务调用移动到 Airport 类中的 beforeInsertbeforeUpdate,以使它能够透明地自动发生。

如何在所有类中共享这个行为呢?我将这些字段和闭包添加到 src/templates 中的默认 DomainClass 模板中。这样,新创建域类时它们就有适当的字段和事件闭包。


结束语

Grails 中的事件能帮助您进一步自定义应用程序运行的方式。可以扩展构建过程,而无需通过在脚本目录中创建一个 Events.groovy 文件来修改标准 Grails 脚本。可以通过向 BootStrap.groovy 文件中的 initdestroy 闭包添加自己的代码来自定义启动和关闭进程。最后,向域类添加 beforeInsertbeforeUpdate 等闭包,这允许您添加时间戳和地理编码等行为。

在下一篇文章中,我将介绍使用 Grails 创建基于数据具象状态传输(Representational State Transfer,REST)的 Web 服务的思想。您将看到 Grails 能轻松支持 HTTP GETPUTPOSTDELETE 操作,而它们是支持下一代 REST 式 Web 服务所需的。到那时,仍然需要精通 Grails。

参考资料

学习

  • 您可以参阅本文在 developerWorks 全球网站上的 英文原文
  • 精通 Grails:阅读此系列丛书,进一步理解 Grails 和它能完成的所有操作。
  • Grails:访问 Grails Web 站点。
  • Grails Framework Reference Documentation:Grails 宝典。
  • Groovy Recipes:在 Scott Davis 的最新著作中了解更多关于 Groovy 和 Grails 的信息。
  • Practically Groovy:这个 developerWorks 系列丛书专门探讨 Groovy 的实际应用,并指导您如何成功它们。
  • Groovy:在项目网站上学习更多关于 Groovy 的信息。
  • AboutGroovy.com:随时关注最新的 Groovy 新闻和文章链接。
  • 用 Gant 构建软件”(Andrew Glover,developerWorks,2008 年 5 月):阅读此教程,了解结合 Groovy 和 Apache Ant 如何使灵活构建更简单。
  • 浏览 技术书店,查阅关于这个主题和其他技术主题的图书。
  • developerWorks Java 技术专区:提供了几百篇有关 Java 编程各个方面的文章。

获得产品和技术

  • Grails:下载最新的 Grails 发行版。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development, Open source
ArticleID=335931
ArticleTitle=精通 Grails: Grails 事件模型
publish-date=09122008