讀古今文學網 > Java程序員修煉之道 > 13.4 Grails快速啟動項目 >

13.4 Grails快速啟動項目

這一節會介紹一個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定義一些屬性,比如strengthdexteritycharisma。有了這些屬性,你就可以開始構想遊戲中的角色如何跟想像的世界交互2。但剛剛看過第11章,你當然想先寫測試!

2 Gweneth是不是應該善於摔跤、雜耍,或面帶微笑地解除對手的武裝?

13.4.2 測試驅動開發

按TDD的方式,我們要先寫個失敗測試,然後實現PlayerCharacter讓測試通過。

我們還準備利用Grails的域對像自動校驗特性。在Grails中,可以自動在任何域對像上調用validate方法,以確保該對象的有效性。代碼清單13-1會測試strengthdexteritycharisma三項統計量都是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測試一樣,它也有setUptearDown方法。但為了在單元測試階段用Grails內置的validate方法,必須通過mockForConstraintsTest方法把它拉進來❷。這是因為Grails把validate當做集成測試的關注點,通常只有這樣才能用它。但如果想要更快地得到反饋,可以把它放到單元測試階段。接下來,可以調用 validate來檢查域對象是否有效❸❹。

現在可以執行下面的命令來運行測試了:

grails test-app
  

這個命令既運行單元測試也會運行集成測試(不過我們現在只有單元測試),並且從控制台中的輸出來看,測試失敗了。

要瞭解測試失敗的原因,需要到target/test-reports/plain目錄下去找。對於這個程序,要找到TEST-unit-unit-com.java7developer.chapter13.PlayerCharacterTests.txt文件。這個文件會告訴你測試失敗是因為在嘗試創建新的PlayerCharacter時,沒找到匹配的構造方法。這很容易理解,因為PlayerCharacter域對像還什麼都沒有呢!

現在你可以把PlayerCharacter搭起來,重複運行測試直到通過。按你的想法加上strengthdexteritycharisma三個屬性。但為了在這些屬性上設定minimum (3)maximum(18)的限制,需要用特殊的限定語法。那樣就可以用Grails提供的默認validate方法了。

Grails中的限定

Grails中的限定是在Spring validator API基礎上實現的。可以用它們指定域類型屬性的校驗需求。Grails的限定很多(在代碼清單13-2中用到了minmax),你還可以根據需要自行編寫限定。參見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方法要檢查的minmax值❸。

PlayerCharacter類變具體後,測試應該能很痛快地通過了(再次運行grails test-app)。如果遵循TDD方式,到這個階段就該著手重構PlayerCharacter和測試了,以便讓代碼更加清爽。

Grails還會確保域對像保存到數據存儲中。

13.4.3 域對像持久化

持久化是由Grails自動處理的,因為Grails認為類中所有具有明確類型的域變量都應該保存到數據庫中。Grails會自動把域對像映射到同名的表中。對於PlayerCharacter域對像而言,三個屬性(strengthdexteritycharisma)全部是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快速開發。當然,還有很多可以對默認行為進行定制的方法值得探索。現在我們就去看看吧。