程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 精通Grails: Grails 事件模型

精通Grails: Grails 事件模型

編輯:關於JAVA

對於事件驅動的反應性開發,構建 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。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved