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
添加 firstName 和 lastName 属性:
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/index,show() 映射到 /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,建议先阅读这篇教程。
先测试 StudentController 的 index() 操作。模拟 StudentService 的 list() 方法,验证返回模型:
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仓库获取。