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 所示:
target ('default': "Cleans a Grails project") { clean() cleanTestReports() } |
可見,它的內容並不多。首先運行 clean 目標,然後運行 cleanTestReports 目標。調用堆棧后,看一下 clean 目標,如清單 2 所示:
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 所示的代碼:
eventCleanStart = { println "### About to clean" } eventCleanEnd = { println "### Cleaning complete" } |
如果輸入 grails clean,應該看到類似於清單 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 所示:
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:
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 所示:
$ 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:
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 所示:
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 所示:
class Hotel{ String name } |
現在構建一個 HotelController,如清單 11 所示:
class HotelController { def scaffold = Hotel } |
注意:如果像 “Grails 與遺留資料庫” 中討論的那樣禁用 grails-app/conf/DataSource.groovy 中的 dbCreate 變數,本例則應該重新添加它並設置為 update。當然,還有另一種選擇是通過手動方式讓 Hotel 表與 Hotel 類的更改保持一致。
現在將清單 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)。然後執行以下步驟:
BootStrap.groovy 中的防故障資料庫插入和刪除
在 BootStrap.groovy 中執行資料庫插入和刪除操作時可能需要一定的防故障措施。如果在插入之前沒有檢查記錄是否存在,可能會在資料庫中得到重複項。如果試著刪除不存在的記錄,會看到在控制台上拋出惡意異常。清單 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 啟動時讓特定於環境的行為發生:
|
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 所示:
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 所示:
excludedProps = ['dateCreated','lastUpdated', 'version', 'id', Events.ONLOAD_EVENT, Events.BEFORE_DELETE_EVENT, Events.BEFORE_INSERT_EVENT, Events.BEFORE_UPDATE_EVENT] |
這會限制在創建和編輯視圖中創建欄位,但仍然在列表中保留欄位並顯示視圖。創建一兩個 Hotel 並驗證欄位會自動更新。
如果應用程序已經使用這些欄位名稱,可以輕鬆地禁用此功能,如清單 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 所示:
class Hotel{ Date dateCreated Date lastUpdated static mapping = { columns { dateCreated column: "cr_time" lastUpdated column: "up_time" } } } |
另一種方案是將域類中的欄位命名為與企業列名稱匹配的名稱,並創建 beforeInsert 和 beforeUpdate 閉包來填充欄位,如清單 19 所示(不要忘記將新欄位設置為 nullable — 否則 save() 方法會在 BootStrap.groovy 中靜默失敗)。
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)
[火星人 ] 精通 Grails: Grails 事件模型已經有661次圍觀