首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》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_grailsgrails 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.chapter13import grails.test.*class PlayerCharacterTests extends GrailsUnitTestCase { //❶扩展GrailsUnitTestCasePlayerCharacter 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.chapter13class 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.PlayerCharacterclass 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.chapter13class 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 //① 用于PlayerCharacter的脚手架}  

PlayerCharacterController类非常简单。依照惯例将域对象的名称赋值给脚手架变量①,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快速开发。当然,还有很多可以对默认行为进行定制的方法值得探索。现在我们就去看看吧。