這一節會介紹一個Grails快速啟動項目,重點展示Grails作為快速Web框架的亮點。用Grails創建Web應用所需的步驟如下:
- 創建域對像;
- 測試驅動開發;
- 域對象的持久化;
- 創建測試數據;
- 控制器;
- GSP視圖;
- 腳手架和自動化的UI創建;
- 快速開發的周轉時間。
說得具體點,我們準備搞一個角色扮演遊戲1中的基本構件(PlayerCharacter
)。到本節結束的時候,你會創建一個具備以下能力的簡單的域對像(PlayerCharacter
):
- 進行一些運行時測試;
- 預先準備好測試數據;
- 可以保存到數據庫中;
- 具有可以進行CRUD操作的基本UI。
1 想想《龍與地下城》或《指環王》。
Grails節省時間的第一個法寶就是自動創建好項目結構。運行grails create-app <my-project>
命令,馬上就能得到一個可以構建的項目!你需要做的唯一一件事情就是保證能接入互聯網,因為它要下載標準的Grails依賴項(比如Spring、Hibernate、JUnit、Tomcat服務器等)。
Grails用來管理和下載依賴項的工具是Apache Ivy。它下載和管理依賴項的概念跟第12章介紹的Maven非常像。下面這個命令會創建一個叫做pcgen_grails的應用程序,包括一個依照Grails的傳統優化過的項目結構。
grails create-app pcgen_grails
依賴項下載完成,其他自動安裝步驟也完成之後,你應該就會得到一個如圖13-3所示的項目結構。
圖13-3 Grails項目的佈局
有了項目結構就可以開始生產一些能運行的代碼了!首先要創建域對像類。
13.4.1 創建域對像
Grails以域對像為應用程序的核心,因此鼓勵你按域驅動設計(Domain-Driven Design,DDD)的方式來考慮問題1。執行grails create-domain-class
命令可以創建域對象。
1 想瞭解DDD(由Eric Evans提出)的更多內容,請訪問域驅動設計社區(http://domaindrivendesign.org/)。
下面的例子創建了一個PlayerCharacter
類,用來表示遊戲中的角色:
cd pcgen_grails
grails create-domain-class com.java7developer.chapter13.PlayerCharacter
Grails會自動為你創建下面的文件:
- 一個表示域對象的PlayerCharacter.groovy源文件(在目錄grails-app/domain/com/java7developer/chapter13下);
- 開發單元測試用的PlayerCharacterTests.groovy源文件(在目錄test/unit/com/java7developer/chapter13下)。
看,Grails在鼓勵你寫單元測試!
還需要給PlayerCharacter
定義一些屬性,比如strength
、dexterity
和charisma
。有了這些屬性,你就可以開始構想遊戲中的角色如何跟想像的世界交互2。但剛剛看過第11章,你當然想先寫測試!
2 Gweneth是不是應該善於摔跤、雜耍,或面帶微笑地解除對手的武裝?
13.4.2 測試驅動開發
按TDD的方式,我們要先寫個失敗測試,然後實現PlayerCharacter
讓測試通過。
我們還準備利用Grails的域對像自動校驗特性。在Grails中,可以自動在任何域對像上調用validate
方法,以確保該對象的有效性。代碼清單13-1會測試strength
、dexterity
和charisma
三項統計量都是3到18之間的數值。
代碼清單13-1 PlayerCharacter
的單元測試
package com.java7developer.chapter13
import grails.test.*
class PlayerCharacterTests extends GrailsUnitTestCase { //❶擴展GrailsUnitTestCase
PlayerCharacter pc;
protected void setUp {
super.setUp
mockForConstraintsTests(PlayerCharacter) //❷注入validate
}
protected void tearDown {
super.tearDown
}
void testConstructorSucceedsWithValidAttributes {
pc = new PlayerCharacter(3, 5, 18)
assert pc.validate //❸通過校驗
}
void testConstructorFailsWithSomeBadAttributes {
pc = new PlayerCharacter(10, 19, 21)
assertFalse pc.validate //❹校驗失敗
}
}
Grails的單元測試都應該擴展自GrailsUnitTestCase
❶。跟所有標準的JUnit測試一樣,它也有setUp
和tearDown
方法。但為了在單元測試階段用Grails內置的validate
方法,必須通過mockForConstraintsTest
方法把它拉進來❷。這是因為Grails把validate
當做集成測試的關注點,通常只有這樣才能用它。但如果想要更快地得到反饋,可以把它放到單元測試階段。接下來,可以調用 validate
來檢查域對象是否有效❸❹。
現在可以執行下面的命令來運行測試了:
grails test-app
這個命令既運行單元測試也會運行集成測試(不過我們現在只有單元測試),並且從控制台中的輸出來看,測試失敗了。
要瞭解測試失敗的原因,需要到target/test-reports/plain目錄下去找。對於這個程序,要找到TEST-unit-unit-com.java7developer.chapter13.PlayerCharacterTests.txt文件。這個文件會告訴你測試失敗是因為在嘗試創建新的PlayerCharacter
時,沒找到匹配的構造方法。這很容易理解,因為PlayerCharacter
域對像還什麼都沒有呢!
現在你可以把PlayerCharacter
搭起來,重複運行測試直到通過。按你的想法加上strength
、dexterity
和charisma
三個屬性。但為了在這些屬性上設定minimum (3)
和maximum(18)
的限制,需要用特殊的限定語法。那樣就可以用Grails提供的默認validate
方法了。
Grails中的限定
Grails中的限定是在Spring validator API基礎上實現的。可以用它們指定域類型屬性的校驗需求。Grails的限定很多(在代碼清單13-2中用到了
min
和max
),你還可以根據需要自行編寫限定。參見http://grails.org/doc/latest/guide/validation.html瞭解詳情。
下面這段代碼中的PlayerCharacter
類中僅包含了讓它可以通過測試的最基本的屬性和限定。
代碼清單13-2 PlayerCharacter
類
package com.java7developer.chapter13
class PlayerCharacter {
Integer strength ﹃
Integer dexterity
Integer charisma ﹄❶要持久化的類型變量
PlayerCharacter {}
PlayerCharacter(Integer str, Integer dex, Integer cha) {
strength = str ﹃
dexterity = dex
charisma = cha ﹄❷可以通過測試的構造方法
}
static constraints = { ﹃
strength(min:3, max:18)
dexterity(min:3, max:18)
charisma(min:3, max:18)﹄❸用於校驗的限定
}
}
PlayerCharacter
類相當簡單。有三個會自動保存到PlayerCharacter
表中的基本屬性❶。有一個帶三個參數的構造方法❷。那個特殊的static
代碼塊確定了validate
方法要檢查的min
和max
值❸。
PlayerCharacter
類變具體後,測試應該能很痛快地通過了(再次運行grails test-app
)。如果遵循TDD方式,到這個階段就該著手重構PlayerCharacter
和測試了,以便讓代碼更加清爽。
Grails還會確保域對像保存到數據存儲中。
13.4.3 域對像持久化
持久化是由Grails自動處理的,因為Grails認為類中所有具有明確類型的域變量都應該保存到數據庫中。Grails會自動把域對像映射到同名的表中。對於PlayerCharacter
域對像而言,三個屬性(strength
、dexterity
和charisma
)全部是Integer
類型,所以都會映射到PlayerCharacter
表中。Grails默認使用Hibernate,並會提供一個HSQLDB內存數據庫(我們在第11章提到過它,那時用做偽裝測試替身),但你可以用自己的數據源取代默認數據源。
grails-app/conf/DataSource.groovy文件裡是數據源的配置。可以在這裡為每種環境設定數據源。記住,Grails已經在pcgen_grails裡給出了默認使用HSQLDB的實現,所以無需任何修改就可以運行它。但代碼清單13-3中給出了使用其他數據庫的配置供參照。
代碼清單13-3 可能的pcgen_grails
數據源
dataSource {}
environments {
development { dataSource {} }
test { dataSource {} }
production { //生產數據源
dataSource {
dbCreate = \"update\"
driverClassName = \"com.mysql.jdbc.Driver\" //數據庫驅動
url = \"jdbc:mysql://localhost/my_app\" //JDBC連接URL
username = \"root\"
password = \"\"
}
}
}
比如說,可以在生產環境中使用MySQL數據庫,而開發和測試環境中還用HSQLDB。這些都是相當標準的Java數據庫連接(JDBC)配置,你對它們應該已經很熟悉了。
Grails開發者也考慮到了手工創建測試數據的問題,所以他們提供了一種機制,可以在應用啟動時將數據預填充到數據庫中。
13.4.4 創建測試數據
測試數據的創建通常是由Grails的BootStrap
類完成的,它在grails-app/conf/BootStrap.groovy中。只要Grails應用或Servlet容器啟動,就會運行init
方法。這和大多數Java Web框架用的啟動servlet所起的作用是一樣的。
注意 可以用
Bootstrap
類做所有初始化操作,但現在我們主要討論測試數據。
代碼清單13-4在初始化階段生成了兩個PlayerCharacter
域對象,並把它們存到了數據庫裡。
代碼清單13-4 為pcgen_grails
引導測試數據
import com.java7developer.chapter13.PlayerCharacter
class BootStrap {
def init = { servletContext -> //❶在servlet上下文啟動時引導
if (!PlayerCharacter.count) {
new PlayerCharacter(strength: 3, dexterity: 5, charisma: 18).save(failOnError: true)
new PlayerCharacter(strength: 18, dexterity: 10, charisma: 4).save(failOnError: true)
}
}
def destroy = {}
}
每次把代碼部署到Servlet容器中都會執行init
方法(即應用啟動和Grails自動部署時)❶。為了確保不會覆蓋掉任何已有數據,可以對已有的PlayerCharacter
實例執行簡單的count
方法。如果確定沒有實例,可以創建一些。這裡有個很重要的特性:如果有異常拋出,或所構造的對象無法通過校驗,則可以肯定對像不會保存到數據庫中。如果願意,可以在destroy
方法中執行清除操作。
有了一個帶有存儲支持的基本域對像後就可以進入下一階段了:在Web頁面上顯示域對象。為此需要構建一個Grails控制器,你應該不會對這個源自MVC設計模式的術語感到陌生。
13.4.5 控制器
Grails遵循MVC設計模式,用控制器來處理來自客戶端(一般是瀏覽器)的Web請求。Grails的慣例是給每個域對像配一個控制器。
要創建域對像PlayerCharacter
的控制器只需要執行下面這條命令:
grails create-controller com.java7developer.chapter13.PlayerCharacter
重要的是指明域對象的完全限定類名,包括包名。
命令執行完成後應該能發現下面這些文件:
PlayerCharacter
域對象的控制器的PlayerCharacterController.groovy源文件(在grails-app/controller/com/java7developer/chapter13目錄下);開發控制器單元測試的PlayerCharacterControllerTests.groovy源文件(在test/unit/com/java7developer/chapter13目錄下);
grails-app/view/playerCharacter文件夾(稍後會用到)。
控制器以簡單的方式支持REST風格的URL和操作映射。假設要把REST風格的URL http://localhost:8080/pcgen_grails/playerCharacter/list映射到一個返回PlayerCharacter
對像列表的方法上。按照Grails慣例優於傳統的方式可以用最少的源碼把URL映射到PlayerCharacterController
類中。這個URL是由下面這些元素組成的:
- 服務器(http://localhost:8080/);
- 基礎項目(pcgen_grails/);
- 控制器名稱的衍生部分(playerCharacter/);
- 在控制器裡聲明的操作塊變量(list)。
要在代碼中看到這些元素,請用代碼清單13-5替換已有的PlayerCharacterController.groovy源碼。
代碼清單13-5 PlayerCharacterController
package com.java7developer.chapter13
class PlayerCharacterController {
List playerCharacters
def list = {
playerCharacters = PlayerCharacter.list //❶ 返回`PlayerCharacter`對像列表
}
}
使用Grails的慣例處理方式,playerCharacter
的屬性會用在REST風格的URL指向的頁面中❶。
但如果現在就啟動程序,然後訪問http://localhost:8080/pcgen_grails/playerCharacter/list,是不會成功的,因為還沒創建JSP或GSP頁面。現在我們就來解決這個問題。
13.4.6 GSP/JSP頁面
用Grails既可以創建GSP頁面,也可以創建JSP頁面。這一節會創建一個簡單的GSP頁面,用來顯示PlayerCharacter
對象的列表(設計師、Web開發者和HTML/CSS大拿們,現在請移開你們的視線!)
代碼清單13-6是GSP頁面grails-app/view/playerCharacter/list.gsp的代碼。
代碼清單13-6 PlayerCharacter
列表的GSP頁面
<html>
<body>
<h1>PC\'s</h1>
<table>
<thead>
<tr>
<td>Strength</td>
<td>Dexterity</td>
<td>Charisma</td>
</tr>
</thead>
<tbody>
<% playerCharacters.each({ pc -> %> //❶開始循環
<tr>
<td><%=\"${pc?.strength}\"%></td>﹃❷輸出屬性
<td><%=\"${pc?.dexterity}\"%></td>
<td><%=\"${pc?.charisma}\"%></td>//﹄❷輸出屬性
</tr>
<%})%> //❸ 循環結束
</tbody>
</table>
</body>
</html>
HTML非常簡單,關鍵是如何用Groovy腳本。你會注意到我們在第8章介紹的Groovy函數字面值語法,它簡化了集合循環操作❶。接著是對角色屬性的引用(注意安全的null
解引用操作符的使用)❷,然後結束函數字面值❸ 。
既然域對像、控制器和它的顯示頁面都準備好了,接下來就可以啟動Grails應用了!執行下面這條命令即可:
grails run-app
Grails會自動在http://localhost:8080上啟動一個Tomcat,並把pcgen_grails應用部署上去。
警告 很多開發人員已經裝過Tomcat服務器了。如果想同時啟動多個Tomcat實例,就要修改端口號,端口8080只能有一個實例監聽。
如果你打開瀏覽器訪問http://localhost:8080/pcgen_grails/,會看到頁面上列出了PlayerCharacterController
,如圖13-4所示。
圖13-4 pcgen_grails主頁
點擊com.java7developer.chapter13.PlayerCharacterController鏈接,就會進入PlayerCharacter域對象的列表頁。
儘管做這個GSP頁面相當快,但如果框架能幫你做豈不是更好?用Grails的腳手架功能可以迅速做出域對像CRUD頁面的原型。
13.4.7 腳手架和UI的自動化創建
Grails可以用它的腳手架(scaffolding)特性自動創建用來執行域對像CRUD操作的UI。
要使用腳手架特性,請用代碼清單13-7替換PlayerCharacterController.groovy源文件中的代碼:
代碼清單13-7 帶腳手架的PlayerCharacterController
package com.java7developer.chapter13
class PlayerCharacterController {
def scaffold = PlayerCharacter //1 用於PlayerCharacter的腳手架
}
PlayerCharacterController
類非常簡單。依照慣例將域對象的名稱賦值給腳手架變量1,Grails馬上就可以構建默認UI。
請暫時把list.gsp
改成list_original.gsp
,以防它會妨礙腳手架產生相應的文件。改好之後,刷新http://localhost:8080/pcgen_grails/playerCharacter/list頁面,就會看到自動生成的PlayerCharacter
域對像列表,如圖13-5所示。
圖13-5 PlayerCharacter
實例列表
在這個頁面中也可以創建、更新和刪除PlayerCharacter
對象。請確保添加了兩個PlayerCharacter
域對像記錄,然後進入下一節瞭解與代碼修改的快速周轉有關的內容。
13.4.8 快速周轉的開發
Grails的run-app
命令為Web快速開發中的「快速」貢獻了一點兒特殊的東西。用Grails的run-app
命令運行的應用程序,其源碼會和服務器連接起來。儘管這在生產環境中不是什麼明智之舉(因為會影響性能),但對於開發和測試來說非常重要。
提示 對於生產環境,一般都是用
grails war
創建WAR文件,然後通過標準的開發流程進行部署。
如果Grails應用中的源碼改了,這些變化會自動反映到服務器上1。我們來試試,改一下PlayerCharacter
域對像:在PlayerCharacter.groovy文件中加一個變量name
,存一下。
String name = \'Gweneth the Merciless\'
1 對於大多數源碼來說都是如此,只要沒改出錯來就行。
現在刷新http://localhost:8080/pcgen_grails/playerCharacter/list頁面,就能看到PlayerCharacter
對像上新加了name
屬性這一列。注意到了嗎?不用停Tomcat,不用重新編譯代碼,其他的什麼也不用做。Grails就是靠這種幾乎即時生效的速度確立了它Web快速開發框架的領導地位。
我們對快速啟動項目的介紹就到此為止了,你應該體驗了一把用Grails做Web快速開發。當然,還有很多可以對默認行為進行定制的方法值得探索。現在我們就去看看吧。