这一节会介绍一个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
定义一些属性,比如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.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测试一样,它也有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.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
方法要检查的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.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快速开发。当然,还有很多可以对默认行为进行定制的方法值得探索。现在我们就去看看吧。