Contents

构建带有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

最后,让我们为其添加firstNamelastName属性:

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 将根据控制器名称和操作呈现视图。例如,来自StudentControllerindex()操作将解析为/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 应用程序是什么样子的:

/uploads/grails_mvc_application/1.jpg

如果要将应用程序部署到 servlet 容器,请使用:

grails war

创建一个准备部署的战争神器。