在事件驅動的反應式開發中,構建 Web 應用程式是一門藝術。傳統的 Web 生命週期僅限於 HTTP 請求與響應,應用程式大多數時間處於閒置狀態,等待用戶請求。然而,Grails 不止於此,它提供了豐富的自定義切入點,讓開發者能夠深入事件模型,注入自定義行為,極大地擴展了應用的靈活性。
Grails 專案的建構過程充滿了事件觸發點。從 grails create-app 到 grails run-app 或 grails war,每一條命令在關鍵節點都會拋出事件。這些事件定義在 $GRAILS_HOME/scripts 目錄下的 Gant 腳本中。例如 Clean.groovy 的 clean 目標會在開始與結束時分別觸發 CleanStart 和 CleanEnd 事件。
若直接修改 Grails 安裝目錄下的腳本,升級或遷移時極易產生「在我機器上能跑」的問題。最佳實踐是在專案的 scripts 目錄下建立 Events.groovy,編寫事件監聽器,將自定義邏輯納入版本控制,確保團隊與 CI 環境一致性。
// scripts/Events.groovy
eventCleanStart = {
println "### 開始清理專案..."
}
eventCleanEnd = {
println "### 清理完成"
}
執行 grails clean 即可在控制台看到自定義輸出。同理,CreatedFile 事件在每次執行 create-* 命令時觸發,並傳回新建檔案的完整路徑。利用此特性,我們可以自動將新建檔案加入版本控制:
eventCreatedFile = { fileName ->
"svn add ${fileName}".execute()
println "### ${fileName} 已自動加入 Subversion"
}
快速發現所有事件的方法是搜尋腳本中的 event( 呼叫。在類 Unix 系統下可使用:
grep "event(" $GRAILS_HOME/scripts/*.groovy
常見事件包括 AppLoadStart、ConfigureAppEnd、StatusFinal 等。掌握這些事件後,即可在 Events.groovy 中編寫對應監聽器,高度客製化建構環境。
若需自定義事件,建議將原腳本複製到專案 scripts 目錄,再添加事件觸發。例如在複製的 Clean.groovy 中 CleanEnd 後添加:
event("TestEvent", [new Date(), "自定義訊息"])
並在 Events.groovy 接收:
eventTestEvent = { timestamp, msg ->
println "### ${msg} 發生於 ${timestamp}"
}
應用程式啟動與關閉的生命週期由 grails-app/conf/BootStrap.groovy 掌控。init 閉包在啟動時執行,destroy 在關閉時執行。注意:直接按 Ctrl+C 停止伺服器不會觸發 destroy,需使用 grails interactive 進入互動模式,再用 exit 正常關閉。
常見用法是在 init 中插入基礎資料,destroy 中清理。為避免重複插入或刪除不存在資料報錯,需加入防錯邏輯:
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.delete()
hotel = Hotel.findByName("Sheraton")
if (hotel) hotel.delete()
}
利用 GrailsUtil.environment 可根據運行模式(development、test、production)執行不同邏輯,結合外部 XML 測試資料,可構建強大的測試平台。
GORM 為領域類別提供四個生命週期掛鉤:beforeInsert、beforeUpdate、beforeDelete、onLoad。若類別包含 dateCreated 和 lastUpdated 欄位,GORM 會自動維護時間戳。若需自定義欄位名(如企業規範的 cr_time、up_time),可透過 static mapping 映射,或直接在類別中定義同名欄位並實作 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() }
}
這些掛鉤亦可用於整合外部服務(如地理編碼),並可透過修改 src/templates/artifacts/DomainClass.groovy 讓所有新建領域類別自動擁有此行為。
Grails 事件模型涵蓋建構、應用啟動/關閉、領域類別持久化三大層面。善用 Events.groovy 擴展建構流程,BootStrap.groovy 掌控應用生命週期,領域類別掛鉤實作業務邏輯,能讓應用更靈活、可維護。下一篇將探討如何用 Grails 建構 REST 風格 Web 服務。