1. Overview
1.概述
In this tutorial, we’ll learn how to create a simple web application using Grails.
在本教程中,我们将学习如何使用Grails创建一个简单的Web应用程序。
Grails (more precisely it’s latest major version) is a framework built on top of the Spring Boot project and uses the Apache Groovy language to develop web apps.
Grails(更确切地说,它的最新主要版本)是一个建立在Spring Boot项目之上的框架,使用Apache Groovy语言来开发网络应用。
It’s inspired by the Rails Framework for Ruby and is built around the convention-over-configuration philosophy which allows reducing boilerplate code.
它的灵感来自于Ruby的Rails框架,并且是围绕着约定俗成的配置理念而建立的,可以减少模板代码。
2. Setup
2.设置
First of all, let’s head over to the official page to prepare the environment. At the time of this tutorial, the latest version is 3.3.3.
首先,让我们到官方网页上准备环境。在编写本教程时,最新版本是3.3.3。
Simply put, there are two ways of installing Grails: via SDKMAN or by downloading the distribution and adding binaries to the PATH environment variable.
简单地说,有两种安装Grails的方法:通过SDKMAN或下载发行版并将二进制文件添加到PATH环境变量中。
We won’t cover the setup step by step because it is well documented in the Grails Docs.
我们不会一步一步地介绍设置,因为在Grails 文档中已经有很好的记录。
3. Anatomy of a Grails App
3.Grails应用程序的剖析</b
In this section, we will get a better understanding of the Grails application structure. As we mentioned earlier, Grails prefers convention over configuration, therefore the location of files defines their purpose. Let’s see what we have in the grails-app directory:
在本节中,我们将对Grails应用程序的结构有一个更好的了解。正如我们前面提到的,Grails更喜欢惯例而不是配置,因此文件的位置决定了它们的用途。让我们看看我们在grails-app目录下有什么。
- assets – a place where we store static assets files like styles, javascript files or images
- conf – contains project configuration files:
- application.yml contains standard web app settings like data source, mime types, and other Grails or Spring related settings
- resources.groovy contains spring bean definitions
- logback.groovy contains logging configuration
- controllers – responsible for handling requests and generating responses or delegating them to the views. By convention, when a file name ends with *Controller, the framework creates a default URL mapping for each action defined in the controller class
- domain – contains the business model of the Grails application. Each class living here will be mapped to database tables by GORM
- i18n – used for internationalization support
- init – an entry point of the application
- services – the business logic of the application will live here. By convention, Grails will create a Spring singleton bean for each service
- taglib – the place for custom tag libraries
- views – contains views and templates
4. A Simple Web Application
4.一个简单的网络应用</b
In this chapter, we will create a simple web app for managing Students. Let’s start by invoking the CLI command for creating an application skeleton:
在本章中,我们将为管理学生创建一个简单的网络应用。让我们从调用CLI命令开始,创建一个应用程序的骨架。
grails create-app
When the basic structure of the project has been generated, let’s move on to implementing actual web app components.
当项目的基本结构生成后,让我们继续实现实际的Web应用组件。
4.1. Domain Layer
4.1.领域层
As we are implementing a web application for handling Students, let’s start with generating a domain class called Student:
由于我们正在实现一个处理学生的网络应用,让我们从生成一个名为Student的域类开始。
grails create-domain-class com.baeldung.grails.Student
And finally, let’s add the firstName and lastName properties to it:
最后,让我们把firstName和lastName属性加入其中。
class Student {
String firstName
String lastName
}
Grails applies its conventions and will set up an object-relational mapping for all classes located in grails-app/domain directory.
Grails应用其惯例,将为位于 grails-app/domain目录下的所有类建立一个对象关系映射。
Moreover, thanks to the GormEntity trait, all domain classes will have access to all CRUD operations, which we’ll use in the next section for implementing services.
此外,由于GormEntity 特质,所有域类将可以访问所有CRUD操作,我们将在下一节中使用这些操作来实现服务。
4.2. Service Layer
4.2.服务层
Our application will handle the following use cases:
我们的应用程序将处理以下用例。
- Viewing a list of students
- Creating new students
- Removing existing students
Let’s implement these use cases. We will start by generating a service class:
让我们来实现这些用例。我们将从生成一个服务类开始。
grails create-service com.baeldung.grails.Student
Let’s head over to the grails-app/services directory, find our newly created service in the appropriate package and add all necessary methods:
让我们前往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()
}
}
Note that services don’t support transactions by default. We can enable this feature by adding the @Transactional annotation to the class.
注意,服务默认不支持交易。我们可以通过向该类添加@Transactional注解来启用这一功能。
4.3. Controller Layer
4.3.控制器层
In order to make the business logic available to the UI, let’s create a StudentController by invoking the following command:
为了使业务逻辑对用户界面可用,让我们通过调用以下命令来创建一个StudentController。
grails create-controller com.baeldung.grails.Student
By default, Grails injects beans by names. It means that we can easily inject the StudentService singleton instance into our controller by declaring an instance variable called studentsService.
默认情况下,Grails按名称注入Bean。这意味着我们可以通过声明一个名为studentService的实例变量,轻松地将StudentService单例注入我们的控制器中。
We can now define actions for reading, creating and deleting students.
我们现在可以定义阅读、创建和删除学生的行动。
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"
}
}
By convention, the index() action from this controller will be mapped to the URI /student/index, the show() action to /student/show and so on.
根据惯例,该控制器的index()动作将被映射到URI /student/index,show()动作被映射到/student/show等等。
4.4. View Layer
4.4.视图层
Having set up our controller actions, we can now proceed to create the UI views. We will create three Groovy Server Pages for listing, creating and removing Students.
设置好控制器动作后,我们现在可以开始创建用户界面视图。我们将创建三个Groovy服务器页面,用于列出、创建和删除学生。
By convention, Grails will render a view based on controller name and action. For example, the index() action from StudentController will resolve to /grails-app/views/student/index.gsp
按照惯例,Grails会根据控制器的名称和动作来渲染视图。例如, index()来自StudentController的动作将解析为/grails-app/views/student/index.gsp。
Let’s start with implementing the view /grails-app/views/student/index.gsp, which will display a list of students. We’ll use the tag <f:table/> to create an HTML table displaying all students returned from the index() action in our controller.
让我们从实现视图/grails-app/views/student/index.gsp开始,它将显示一个学生的列表。我们将使用标签<f:table/>来创建一个HTML表格,显示从我们控制器中的index()动作返回的所有学生。
By convention, when we respond with a list of objects, Grails will add the “List” suffix to the model name so that we can access the list of student objects with the variable studentList:
按照惯例,当我们用一个对象的列表进行响应时,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>
We’ll now proceed to the view /grails-app/views/student/create.gsp, which allows the user to create new Students. We’ll use the built-in <f:all/> tag, which displays a form for all properties of a given bean:
现在我们将进入视图/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>
Finally, let’s create the view /grails-app/views/student/show.gsp for viewing and eventually deleting students.
最后,让我们创建视图/grails-app/views/student/show.gsp,用于查看和最终删除学生。
Among other tags, we’ll take advantage of <f:display/>, which takes a bean as an argument and displays all its fields:
在其他标签中,我们将利用<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. Unit Tests
4.5.单元测试
Grails mainly takes advantage of Spock for testing purposes. If you are not familiar with Spock, we highly recommend reading this tutorial first.
Grails主要利用Spock进行测试。如果您不熟悉Spock,我们强烈建议您先阅读本教程。
Let’s start with unit testing the index() action of our StudentController.
让我们从单元测试StudentController的index()/em>动作开始。StudentController的index()动作。
We’ll mock the list() method from StudentService and test if index() returns the expected model:
我们将模拟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'
}
Now, let’s test the delete() action. We’ll verify if delete() was invoked from StudentService and verify redirection to the index page:
现在,让我们测试一下delete() 动作。我们将验证delete()是否被从StudentService调用,并验证重定向到索引页。
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. Integration Tests
4.6.集成测试
Next, let’s have a look at how to create integration tests for the service layer. Mainly we’ll test integration with a database configured in grails-app/conf/application.yml.
接下来,让我们看一下如何为服务层创建集成测试。主要我们将测试与grails-app/conf/application.yml中配置的数据库的集成。
By default, Grails uses the in-memory H2 database for this purpose.
默认情况下,Grails使用内存中的H2数据库来实现这一目的。
First of all, let’s start with defining a helper method for creating data to populate the database:
首先,让我们开始定义一个用于创建数据以填充数据库的辅助方法。
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
}
Thanks to the @Rollback annotation on our integration test class, each method will run in a separate transaction, which will be rolled back at the end of the test.
由于我们的集成测试类上的@Rollback注解,每个方法将在一个单独的事务中运行,在测试结束时将被回滚。
Take a look at how we implemented the integration test for our list() method:
看看我们是如何为我们的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'
}
Also, let’s test the delete() method and validate if the total count of students is decremented by one:
另外,让我们测试一下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. Running and Deploying
5.运行和部署</b
Running and deploying apps can be done by invoking single command via Grails CLI.
运行和部署应用程序可以通过Grails CLI调用单个命令来完成。
For running the app use:
对于运行该应用程序,请使用。
grails run-app
By default, Grails will setup Tomcat on port 8080.
默认情况下,Grails将在端口8080上设置Tomcat。
Let’s navigate to http://localhost:8080/student/index to see what our web application looks like:
让我们导航到http://localhost:8080/student/index,看看我们的网络应用是什么样子。
If you want to deploy your application to a servlet container, use:
如果你想把你的应用程序部署到一个servlet容器中,请使用。
grails war
to create a ready-to-deploy war artifact.
来创建一个准备部署的战争构件。
6. Conclusion
6.结论
In this article, we focused on how to create a Grails web application using the convention-over-configuration philosophy. We also saw how to perform unit and integration tests with the Spock framework.
在这篇文章中,我们重点讨论了如何使用约定俗成的理念来创建一个Grails网络应用。我们还看到了如何使用Spock框架进行单元和集成测试。
As always, all the code used here can be found over on GitHub.
一如既往,这里使用的所有代码都可以在GitHub上找到。