天天看点

精通 Grails: Grails 事件模型,自定义贯穿应用程序生命周期的行为

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

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

构建事件

开发 Grails 的第一步是输入

grails create-app

。最后输入

grails run-app

grails war

。这期间输入的所有命令和内容都会在过程的关键点抛出事件。

查看 $GRAILS_HOME/scripts 目录。此目录中的文件是 Gant 脚本,对应输入的命令。例如,输入

grails clean

时,调用 Clean.groovy。

精通 Grails: Grails 事件模型,自定义贯穿应用程序生命周期的行为

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-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)。然后执行以下步骤:

  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 启动时让特定于环境的行为发生:

精通 Grails: 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 个事件挂钩:

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。