1. 概述
在本教程中,我们将学习如何使用Grails创建一个简单的Web应用程序。
Grails(更准确地说是它的最新主要版本)是一个建立在Spring Boot项目之上的框架,使用Apache Groovy语言来开发Web应用程序。
它受到Ruby的Rails框架的启发,并围绕约定优于配置的理念构建,从而可以减少样板代码。
2. 设置
首先,让我们前往官方页面准备环境。在本教程撰写时,最新版本是3.3.3。
简而言之,有两种安装Grails的方法:通过SDKMAN或下载发行版并将二进制文件添加到PATH环境变量中。
我们不会逐步介绍设置过程,因为Grails Docs中已有详细记录。
3. Grails应用程序的剖析
在本节中,我们将更好地了解Grails应用程序结构。正如我们前面提到的,Grails更倾向于约定而不是配置,因此文件的位置定义了它们的用途。让我们看看grails-app目录中有什么:
-
assets:存储静态资产文件(如样式、JavaScript文件或图像)的地方
-
conf:包含项目配置文件:
- application.yml包含标准Web应用程序设置,例如数据源、MIME类型以及其他Grails或Spring相关设置
- resources.groovy包含Spring Bean定义
- logback.groovy包含日志配置
-
controllers:负责处理请求并生成响应或将其委托给视图,按照惯例,当文件名以*Controller结尾时,框架会为控制器类中定义的每个操作创建一个默认URL映射
-
domain:包含Grails应用程序的业务模型,这里的每个类都将由GORM映射到数据库表
-
i18n:用于国际化支持
-
init:应用程序的入口点
-
services:应用程序的业务逻辑将在此处存在,按照惯例,Grails将为每个服务创建一个Spring单例Bean
-
taglib:自定义标签库的位置
-
views:包含视图和模板
4. 简单的Web应用程序
在本章中,我们将创建一个简单的Web应用程序来管理学生;让我们首先调用CLI命令来创建应用程序骨架:
grails create-app
当项目的基本结构生成后,让我们继续实现实际的Web应用程序组件。
4.1 域层
因为我们正在实现一个用于处理学生的Web应用程序,所以我们首先要生成一个名为Student的域类:
grails create-domain-class cn.tuyucheng.taketoday.grails.Student
最后,让我们添加firstName和lastName属性:
class Student {
String firstName
String lastName
}
Grails应用其约定并将为位于grails-app/domain目录中的所有类设置对象关系映射。
此外,由于GormEntity特性,所有域类都可以访问所有CRUD操作,我们将在下一节中使用这些操作来实现Service。
4.2 服务层
我们的应用程序将处理以下用例:
- 查看学生列表
- 创建新学生
- 删除现有学生
让我们实现这些用例,我们将首先生成一个Service类:
grails create-service cn.tuyucheng.taketoday.grails.Student
让我们转到grails-app/services目录,在适当的包中找到我们新创建的Service并添加所有必要的方法:
@Transactional
class StudentService {
def get(id){
Student.get(id)
}
def list() {
Student.list()
}
def save(student){
student.save()
}
def delete(id){
Student.get(id).delete()
}
}
请注意,Service默认不支持事务,我们可以通过在类中添加@Transactional注解来启用此功能。
4.3 控制器层
为了使业务逻辑可用于UI,让我们通过调用以下命令创建一个StudentController:
grails create-controller cn.tuyucheng.taketoday.grails.Student
默认情况下,Grails通过名称注入Bean,这意味着我们可以通过声明一个名为studentsService的实例变量轻松地将StudentService单例实例注入到我们的控制器中。
我们现在可以定义读取、创建和删除学生的操作。
class StudentController {
def studentService
def index() {
respond studentService.list()
}
def show(Long id) {
respond studentService.get(id)
}
def create() {
respond new Student(params)
}
def save(Student student) {
studentService.save(student)
redirect action:"index", method:"GET"
}
def delete(Long id) {
studentService.delete(id)
redirect action:"index", method:"GET"
}
}
按照惯例,此控制器的index()操作将映射到URI /student/index,show()操作将映射到URI /student/show,依此类推。
4.4 视图层
设置完控制器操作后,我们现在可以继续创建UI视图。我们将创建3个Groovy服务器页面,用于列出、创建和删除学生。
按照惯例,Grails将根据控制器名称和操作呈现视图。例如,StudentController中的index()操作将解析为/grails-app/views/student/index.gsp。
让我们从实现视图/grails-app/views/student/index.gsp开始,该视图将显示学生列表。我们将使用标签<f:table/>创建一个HTML表格,显示从控制器中的index()操作返回的所有学生。
按照惯例,当我们使用对象列表进行响应时,Grails会在模型名称中添加“List”后缀,以便我们可以使用变量studentList访问学生对象列表:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
</head>
<body>
<div class="nav" role="navigation">
<ul>
<li><g:link class="create" action="create">Create</g:link></li>
</ul>
</div>
<div id="list-student" class="content scaffold-list" role="main">
<f:table collection="${studentList}"
properties="['firstName', 'lastName']" />
</div>
</body>
</html>
现在我们进入视图/grails-app/views/student/create.gsp,该视图允许用户创建新的学生。我们将使用内置的<f:all/>标签,该标签显示给定Bean的所有属性的表单:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
</head>
<body>
<div id="create-student" class="content scaffold-create" role="main">
<g:form resource="${this.student}" method="POST">
<fieldset class="form">
<f:all bean="student"/>
</fieldset>
<fieldset class="buttons">
<g:submitButton name="create" class="save" value="Create" />
</fieldset>
</g:form>
</div>
</body>
</html>
最后,让我们创建视图/grails-app/views/student/show.gsp来查看并最终删除学生。
除其他标签外,我们将利用<f:display/>,它以Bean作为参数并显示其所有字段:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
</head>
<body>
<div class="nav" role="navigation">
<ul>
<li><g:link class="list" action="index">Students list</g:link></li>
</ul>
</div>
<div id="show-student" class="content scaffold-show" role="main">
<f:display bean="student" />
<g:form resource="${this.student}" method="DELETE">
<fieldset class="buttons">
<input class="delete" type="submit" value="delete" />
</fieldset>
</g:form>
</div>
</body>
</html>
4.5 单元测试
Grails主要利用Spock进行测试,如果你不熟悉Spock,我们强烈建议你先阅读本教程。
让我们开始对StudentController的index()操作进行单元测试。
我们将Mock StudentService中的list()方法并测试index()是否返回预期的模型:
void "Test the index action returns the correct model"() {
given:
controller.studentService = Mock(StudentService) {
list() >> [new Student(firstName: 'John',lastName: 'Doe')]
}
when:"The index action is executed"
controller.index()
then:"The model is correct"
model.studentList.size() == 1
model.studentList[0].firstName == 'John'
model.studentList[0].lastName == 'Doe'
}
现在,让我们测试delete()操作;我们将验证delete()是否从StudentService调用,并验证重定向到index页面:
void "Test the delete action with an instance"() {
given:
controller.studentService = Mock(StudentService) {
1 * delete(2)
}
when:"The domain instance is passed to the delete action"
request.contentType = FORM_CONTENT_TYPE
request.method = 'DELETE'
controller.delete(2)
then:"The user is redirected to index"
response.redirectedUrl == '/student/index'
}
4.6 集成测试
接下来,我们来看看如何创建服务层的集成测试,我们主要测试与grails-app/conf/application.yml中配置的数据库的集成。
默认情况下,Grails使用内存H2数据库来实现此目的。
首先,让我们开始定义一个用于创建数据来填充数据库的辅助方法:
private Long setupData() {
new Student(firstName: 'John',lastName: 'Doe')
.save(flush: true, failOnError: true)
new Student(firstName: 'Max',lastName: 'Foo')
.save(flush: true, failOnError: true)
Student student = new Student(firstName: 'Alex',lastName: 'Bar')
.save(flush: true, failOnError: true)
student.id
}
得益于我们集成测试类上的@Rollback注解,每种方法将在单独的事务中运行,并将在测试结束时回滚。
看看我们如何实现list()方法的集成测试:
void "test list"() {
setupData()
when:
List<Student> studentList = studentService.list()
then:
studentList.size() == 3
studentList[0].lastName == 'Doe'
studentList[1].lastName == 'Foo'
studentList[2].lastName == 'Bar'
}
另外,让我们测试一下delete()方法并验证学生总数是否减少了1:
void "test delete"() {
Long id = setupData()
expect:
studentService.list().size() == 3
when:
studentService.delete(id)
sessionFactory.currentSession.flush()
then:
studentService.list().size() == 2
}
5. 运行和部署
通过Grails CLI调用单个命令即可运行和部署应用程序。
运行应用程序使用:
grails run-app
默认情况下,Grails将在端口8080上设置Tomcat。
让我们导航到http://localhost:8080/student/index来查看我们的Web应用程序是什么样子的:
如果要将应用程序部署到Servlet容器,请使用:
grails war
创建一个可随时部署的WAR工件。
6. 总结
在本文中,我们重点介绍了如何使用约定优于配置的理念创建Grails Web应用程序,我们还了解了如何使用Spock框架执行单元测试和集成测试。
Post Directory
