构建带有Grails的MVC Web应用程序
1. 概述
在本教程中,我们将学习如何使用Grails 创建一个简单的 Web 应用程序。
Grails(更准确地说是最新的主要版本)是一个构建在 Spring Boot 项目之上的框架,并使用 Apache Groovy 语言开发 Web 应用程序。
它受到 Rails Ruby 框架的启发,并围绕约定优于配置的理念构建,该理念允许减少样板代码。
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 com.blogdemo.grails.Student
最后,让我们为其添加firstName和lastName属性:
class Student {
String firstName
String lastName
}
Grails 应用其约定并将为位于grails-app/domain目录中的所有类设置对象关系映射。
此外,由于GormEntity 特征,所有域类都可以访问所有 CRUD 操作,我们将在下一节中使用这些操作来实现服务。
4.2. 服务层
我们的应用程序将处理以下用例:
- 查看学生列表
- 创建新学生
- 删除现有学生
让我们实现这些用例。我们将从生成一个服务类开始:
grails create-service com.blogdemo.grails.Student
让我们转到grails-app/services目录,在适当的包中找到我们新创建的服务并添加所有必要的方法:
@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()
}
}
请注意,服务默认不支持事务。我们可以通过在类中添加@Transactional注解来启用这个特性。
4.3. 控制器层
为了使 UI 可以使用业务逻辑,让我们通过调用以下命令创建一个StudentController :
grails create-controller com.blogdemo.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()操作映射到/student/show等等。
4.4. 查看图层
设置好控制器动作后,我们现在可以继续创建 UI 视图。我们将创建三个 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()*动作开始。
我们将模拟 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()操作。我们将验证是否从StudentService调用了*delete()*并验证重定向到索引页面:
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()*方法并验证学生总数是否减一:
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
创建一个准备部署的战争神器。