1. 概述

本教程将带你学习如何使用 Grails 框架构建一个简单的Web应用。

Grails(准确说是其最新主版本)是一个构建在 Spring Boot 项目之上的框架,使用 Apache Groovy 语言开发Web应用。它受 Ruby on Rails 框架启发,核心思想是约定优于配置,能大幅减少样板代码

2. 环境搭建

首先访问官网准备开发环境。教程编写时最新版本是 3.3.3。

简单来说,有两种安装方式:通过 SDKMAN 或下载发行版并将二进制文件添加到 PATH 环境变量。我们不会逐步讲解安装步骤,因为 Grails 官方文档 已有详细说明。

3. Grails 应用解剖

本节将深入理解 Grails 应用结构。如前所述,Grails 遵循约定优于配置原则,因此文件位置决定了其用途。看看 grails-app 目录包含什么:

  • assets – 存放静态资源文件(样式、JS、图片等)
  • conf – 项目配置文件目录:
    • application.yml:标准Web应用配置(数据源、MIME类型等Grails/Spring相关设置)
    • resources.groovy:Spring Bean定义
    • logback.groovy:日志配置
  • controllers – 处理请求并生成响应或委托给视图。约定:文件名以*Controller结尾时,框架会为控制器类中的每个操作创建默认URL映射
  • domain – 业务模型所在目录。此处的每个类会被GORM映射到数据库表
  • i18n – 国际化支持文件
  • init – 应用程序入口点
  • services – 业务逻辑层。约定:Grails会为每个服务创建Spring单例Bean
  • taglib – 自定义标签库
  • views – 视图和模板文件

4. 构建简单Web应用

本章我们将创建一个学生管理Web应用。先用CLI命令生成应用骨架:

grails create-app

项目基础结构生成后,开始实现实际组件。

4.1 领域层

实现学生管理应用,先创建名为 Student 的领域类:

grails create-domain-class com.baeldung.grails.Student

添加 firstNamelastName 属性:

class Student {
    String firstName
    String lastName
}

Grails会自动为 grails-app/domain 目录下的所有类设置对象关系映射。得益于 GormEntity 特性,所有领域类可直接访问所有CRUD操作,后续实现服务层时会用到。

4.2 服务层

应用需要处理以下用例:

  • 查看学生列表
  • 创建新学生
  • 删除现有学生

生成服务类:

grails create-service com.baeldung.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.baeldung.grails.Student

Grails 默认按名称注入Bean。只需声明名为 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/indexshow() 映射到 /student/show,以此类推。

4.4 视图层

控制器操作设置完成后,创建UI视图。我们将用三个Groovy Server Pages实现学生列表、创建和删除功能。

按约定,Grails会根据控制器名和操作名渲染视图。例如 StudentController index() 操作会解析为 /grails-app/views/student/index.gsp

先实现列表视图 /grails-app/views/student/index.gsp,使用 <f:table/> 标签生成HTML表格显示学生数据:

<!DOCTYPE html>
<html>
    <head>
        <meta name="layout" content="main" />
    </head>
    <body>
        <div class="nav" role="navigation">
            <ul>
                <li><g:link class="create" action="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会在模型名后添加"List"后缀,因此可通过变量 studentList 访问学生列表。

接着实现创建视图 /grails-app/views/student/create.gsp,使用内置 <f:all/> 标签生成表单:

<!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="创建" />
                </fieldset>
            </g:form>
        </div>
    </body>
</html>

最后实现详情/删除视图 /grails-app/views/student/show.gsp,使用 <f:display/> 标签显示所有字段:

<!DOCTYPE html>
<html>
    <head>
        <meta name="layout" content="main" />
    </head>
    <body>
        <div class="nav" role="navigation">
            <ul>
                <li><g:link class="list" action="index">学生列表</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="删除" />
                </fieldset>
            </g:form>
        </div>
    </body>
</html>

4.5 单元测试

Grails主要使用 Spock 框架进行测试。如果不熟悉Spock,建议先阅读这篇教程

先测试 StudentControllerindex() 操作。模拟 StudentServicelist() 方法,验证返回模型:

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() 操作,验证是否调用了服务层删除方法并重定向到首页:

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 查看效果:

学生列表页面

要部署到Servlet容器,执行:

grails war

生成可直接部署的WAR包。

6. 总结

本文展示了如何使用约定优于配置原则构建Grails Web应用,并通过Spock框架进行单元测试和集成测试。完整代码可在GitHub仓库获取。


原始标题:Build an MVC Web Application with Grails

« 上一篇: Java Weekly, 第221期
» 下一篇: RxRelay for RxJava 详解