显示导航

创建第一个 Grails 应用程序

了解如何创建你的第一个 Grails 应用

作者:Zachary Klein

Grails 版本 5.0.1

1 Grails 培训

Grails 培训 - 由创建并积极维护 Grails 框架的人员开发并交付!

2 入门

在本指南中,你将创建你的第一个 Grails 应用程序。你将了解领域类、控制器、服务、GSP 以及单元和集成测试。本指南面向 Grails 新手或希望获得有关该框架的复习课程的开发人员。

2.1 你需要什么

要完成此指南,你需要以下内容

  • 一些时间

  • 不错的文本编辑器或 IDE

  • 安装 JDK 1.8 或更高版本,并正确配置了 JAVA_HOME

2.2 如何完成指南

要开始操作,请执行以下操作

Grails 指南代码库包含两个文件夹

  • initial 初始项目。通常是带有附加代码以助你快速入门的简单 Grails 应用程序。

  • complete 完成的示例。这是遵循指南提供步骤并对 initial 文件夹应用这些更改后的结果。

要完成指南,请转到 initial 文件夹

  • cdgrails-guides/creating-your-first-grails-app/initial

并按照下一节中的说明进行操作。

如果你 cdgrails-guides/creating-your-first-grails-app/complete,则可以直接查看完成的示例

3 创建 Grails 应用程序

对于本指南,你可以自行创建起始项目,或者使用指南代码库中包含的 initial 项目来开始操作。如果你选择使用 initial 项目,则可以安全地跳过本部分并继续到 [runningTheApp]。如果你想在电脑上安装 Grails,请遵循以下选项之一。

3.1 安装 Grails

安装 Grails

SDKMan

'sdkman'是一款流行的命令行实用工具,用于安装和管理 Grails 安装(以及其他 JVM 框架和语言)。在 Unix 终端中运行以下命令安装 sdkman

$ curl -s "https://get.sdkman.io" | bash

安装完成后,安装最新版本的 Grails(本指南使用 4.0.1 版)

$ sdk install grails 4.0.1

sdkman 将提示你选择是否将此版本设为默认版本(选择“是”)。

$ grails --version

| Grails Version: 4.0.1
| JVM Version: 1.8.0_77
如果你运行的是 Windows,则可以使用遵循相同约定的 sdkman 克隆项目。你可以从 https://github.com/flofreud/posh-gvm 下载

3.2 Grails应用程序Forge

您知道无需安装任何其他工具,就可以下载完整的Grails项目吗?前往start.grails.org并使用Grails应用程序Forge生成您的Grails项目。您可以选择项目类型(应用程序或插件)、选择Grails版本,并选择个人资料,然后单击“生成项目”以下载ZIP文件。无需Grails安装!

您甚至可以使用HTTP工具(例如curl)从命令行下载您的项目(有关API文档,请参见start.grails.org

curl -O start.grails.org/myapp.zip -d version=4.0.1

3.3创建应用程序

$ grails create-app myApp

如果您没有指定软件包,则应用程序名称将用作应用程序的默认软件包(例如,myapp)。您可以在grails-app/conf/application.yml中编辑默认软件包,您可以选择为应用程序指定默认软件包

$ grails create-app org.grails.guides.myApp

3.4应用程序配置文件

您可以选择为您的Grails应用程序指定个人资料。配置文件适用于许多常见的应用程序类型,包括rest-apiangularreact等,您甚至可以创建自己的个人资料。

要查看可用的个人资料列表,请使用list-profiles命令。

$ grails list-profiles

| Available Profiles
--------------------
* angular - A profile for creating applications using AngularJS
* rest-api - Profile for REST API applications
* base - The base profile extended by other profiles
* angular2 - A profile for creating Grails applications with Angular 2
* plugin - Profile for plugins designed to work across all profiles
* profile - A profile for creating new Grails profiles
* react - A profile for creating Grails applications with a React frontend
* rest-api-plugin - Profile for REST API plugins
* web - Profile for Web applications
* web-plugin - Profile for Plugins designed for Web applications
* webpack - A profile for creating applications with node-based frontends using webpack

要使用个人资料,请在-profile标志之前指定其名称

grails create-app myApp -profile rest-api

您还可以选择指定软件包和版本(默认为org.grails.profiles和个人资料的当前版本)

grails create-app myApp -profile org.grails.profiles:react:1.0.2

要获取有关个人资料的详细信息,请使用profile-info命令。

$ grails profile-info plugin

Profile: plugin
--------------------
Profile for plugins designed to work across all profiles

Provided Commands:
--------------------
| Error Error occurred loading commands: grails.dev.commands.ApplicationContextCommandRegistry (Use --stacktrace to see the full trace)
| Error Error occurred loading commands: grails.dev.commands.ApplicationContextCommandRegistry (Use --stacktrace to see the full trace)
* package-plugin - Packages the plugin into a JAR file
* publish-plugin - Publishes the plugin to the Grails central repository
* help - Prints help information for a specific command
* open - Opens a file in the project
* gradle - Allows running of Gradle tasks
* clean - Cleans a Grails application's compiled sources
* compile - Compiles a Grails application
* create-command - Creates an Application Command
* create-domain-class - Creates a Domain Class
* create-service - Creates a Service
* create-unit-test - Creates a unit test
* install - Installs a Grails application or plugin into the local Maven cache
* assemble - Creates a JAR or WAR archive for production deployment
* bug-report - Creates a zip file that can be attached to issue reports for the current project
* console - Runs the Grails interactive console
* create-script - Creates a Grails script
* dependency-report - Prints out the Grails application's dependencies
* list-plugins - Lists available plugins from the Plugin Repository
* plugin-info - Prints information about the given plugin
* run-app - Runs a Grails application
* run-command - Executes Grails commands
* run-script - Executes Groovy scripts in a Grails context
* shell - Runs the Grails interactive shell
* stats - Prints statistics about the project
* stop-app - Stops the running Grails application
* test-app - Runs the applications tests

Provided Features:
--------------------
* asset-pipeline - Adds Asset Pipeline to a Grails project
* hibernate4 - Adds GORM for Hibernate 4 to the project
* hibernate5 - Adds GORM for Hibernate 5 to the project
* json-views - Adds support for JSON Views to the project
* less-asset-pipeline - Adds LESS Transpiler Asset Pipeline to a Grails project
* markup-views - Adds support for Markup Views to the project
* mongodb - Adds GORM for MongoDB to the project
* neo4j - Adds GORM for Neo4j to the project
* rx-mongodb - Adds RxGORM for MongoDB to the project
* asset-pipeline-plugin - Adds Asset Pipeline to a Grails Plugin for packaging
当您在没有-profile的情况下创建应用程序时,使用的默认配置文件是web配置文件。

4运行应用程序

现在您已经创建(或下载)了您的Grails项目,现在是时候运行它并看看Grails已经为您提供了什么。

如果您有Grails的本地安装,则可以使用run-app在您的项目中运行该应用程序

$ cd myApp/

在未安装Grails的情况下运行应用程序

由于从Grails版本3.2.3或更高版本开始,有了Grails包装器,所以您可以运行任何Grails命令而无需安装Grails。如果您从Grails应用程序Forge下载它,Grails包装器也将包含在内。

$ ./grailsw run-app

使用Grails运行应用程序

如果您在机器中安装了Grails,只需输入

$ grails run-app

在Grails交互模式下运行应用程序

你还可以使用 Grails 的交互模式来运行 Grails 运行时,可以从中发出任何 Grails 命令,而无需等待运行时为每个任务启动。

在本指南中,我们将优先使用 Grails 包装器处理大多数命令。

$ ./grailsw

| Enter a command name to run. Use TAB for completion:
grails>run-app      //you can shutdown the app with the stop-app command

使用 Gradle 运行应用程序

最后,由于 Grails 构建在Spring BootGradle之上,因此你还可以使用 Spring Boot 命令,例如bootRun,与你的 Grails 应用程序进行交互。这些命令可用作 Gradle 任务。就像 Grails 本身一样,无需在你的设备上安装 Gradle。使用 Gradle 包装器 (gradlew) 时,将自动下载它

$ ./gradlew bootRun

运行上述任何一个命令后,Grails 将使用嵌入式 Tomcat 服务器启动你的应用程序,并使其可用(默认情况下),位于http://localhost:8080

$ ./grailsw run-app

| Running application...
Grails application running at http://localhost:8080 in environment: development

4.1 更改默认端口

如果你想要更改端口号,只需编辑grails-app/conf/下的application.yml文件,并添加以下行

server:
    port: 8090

你可以在命令调用中直接提供端口号

$ ./grailsw run-app --port=8090

| Running application...
Grails application running at http://localhost:8090 in environment: development

4.2 自动重新加载

应用程序目前仅显示一个显示有关应用程序的某些信息的默认索引页。默认索引页位于grails-app/views/index.gsp下。

继续编辑该文件,并更改页面上的某些内容,如下所示(第 54 行)

    <div id="content" role="main">
        <section class="row colset-2-its">
            <h1>Welcome to My First Grails Project</h1>  (1)
1 更改<h1>标签的文本。

保存你的更改并在浏览器中刷新页面。你应该会在主页上看到你的更改。当 Grails 检测到更改时,它将自动重新加载视图、控制器、领域类和其他制品的更改,因此你无需在每次更改后重新启动应用程序。

对领域类关联、类重命名以及影响应用程序“接线”的其他操作所做的重大更改可能无法成功重新加载。

5 领域类

Grails 是基于 Spring Boot 项目的模型视图控制器 (MVC) 框架。通常,一个 MVC 应用程序将应用程序的职责划分为三个类别

  1. 模型 - 定义和管理数据的代码

  2. 视图 - 管理数据演示的代码(例如,HTML 页面)

  3. 控制器 - 定义 Web 应用程序的逻辑以及管理模型和视图之间的通信的代码。控制器响应请求,从模型获取数据,并将数据传递到视图。

通常,面向对象 MVC 框架要求开发人员配置哪些类对应于上面的三类中的每一类。但是,Grails 在很多框架中更进一步,它遵循“Convention over Configuration(约定优于配置)”的开发方法。这意味着对于 Grails 中的许多构件类型(控制器、视图等),你只需在项目中的特定目录中创建一个文件,Grails 就会将它自动连接到你的应用程序中,无需进一步配置。

Grails Object Relational Mapper(GORM)负责将领域类映射到数据库表(和其他持久化存储)。GORM 是 Grails 框架中一个强大的工具,甚至可以独立于 Grails 项目在外部使用。它支持关系数据库(通过 Hibernate),以及 MongoDb、Neo4j、Redis 和 Cassandra 数据源。请参阅 GORM 文档 以了解更多信息。

在构建 MVC 应用程序时,通常从“M”(模型)开始,也称为“领域模型”。在 Grails 中,你的领域模型定义在 grails-app/domain 下的 Groovy 类中。我们来创建一个领域类。

5.1 创建领域类

领域类可以由 Grails 生成(在这种情况下,Grails 会自动创建一个单元测试),或者你可以自己创建文件。

$ ./grailsw create-domain-class Vehicle

| Created grails-app/domain/org/grails/guides/Vehicle.groovy
| Created src/test/groovy/org/grails/guides/VehicleSpec.groovy

这将生成两个 Groovy 文件,一个是我们自己的领域类,另一个是单元测试。我们来看看我们的领域类是什么样的。

package org.grails.guides

class Vehicle {

    static constraints = {
    }
}

现在我们的领域类没有属性,没有约束。这并不有趣,但值得注意的是,这是在我们的应用程序中连接持久化领域类所需要的全部内容。默认情况下,Hibernate 将用于配置数据源(默认情况下是内存中的 H2 数据库),并为 grails-app/domain 下的所有 Groovy 类创建表和关联。我们为该领域类添加一些属性

package org.grails.guides

class Vehicle {

    String name (1)

    String make
    String model

    static constraints = { (2)
        name maxSize: 255
        make inList: ['Ford', 'Chevrolet', 'Nissan']
        model nullable: true
    }
}
1 属性将用于创建数据库中的列(假设使用关系数据库)
2 约束用于强制每个字段中的有效数据 - Grails 为常见场景提供了一组丰富的约束,你也可以定义自定义约束

请参阅 Grails 文档,了解 领域类约束 的完整列表和文档

5.2 数据库控制台

如果你再次运行该应用程序,你应该会看到与之前相同的页面。但是,你可以登录到数据库控制台并查看你的新数据库表。

浏览至 http://localhost:8080/dbconsole 并登录。默认用户名为 sa,没有密码。默认 JDBC URL 是:jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

DB Console
您可以在 application.ymlenvironments development dataSource url 下查看 JDBC url

登录 DB 控制台后,您应该在左边的侧边栏看到新的 VEHICLES 表。点击 + 图标展开表格 - 您应该看到列的列表,包括我们刚定义的三个 String 字段,namemakemodel

DB Console

5.3 扩展领域模型

对于我们的 Vehicle 类,makemodel 是普通字符串不太合理,因为型号实际上应与制造商相关联。让我们更新我们的领域模型,使其更加强大。

创建两个新的领域类

$ ./grailsw create-domain-class Make

| Created grails-app/domain/org/grails/guides/Make.groovy
| Created src/test/groovy/org/grails/guides/Make.groovy

$ ./grailsw create-domain-class Model

| Created grails-app/domain/org/grails/guides/Model.groovy
| Created src/test/groovy/org/grails/guides/Model.groovy

使用以下内容编辑这两个文件

grails-app/domain/org/grails/guides/Make.groovy
package org.grails.guides

class Make {

    String name

    static constraints = {
    }

    String toString() {
        name
    }
}
grails-app/domain/org/grails/guides/Model.groovy
package org.grails.guides

class Model {

    String name

    static belongsTo = [ make: Make ]

    static constraints = {
    }

    String toString() {
        name
    }
}
belongsTo 属性是 GORM 用于确定领域类之间关联的几个属性之一。其他属性包括 hasManyhasOne。更多信息请参阅 GORM 文档

现在,更新 Vehicle.groovy ,以便使用新的 MakeModel 类来替换 String

grails-app/domain/org/grails/guides/Vehicle.groovy
package org.grails.guides

@SuppressWarnings('GrailsDomainReservedSqlKeywordName')
class Vehicle {

    Integer year

    String name
    Model model
    Make make

    static constraints = {
        year min: 1900
        name maxSize: 255
    }
}

Grails(通过 GORM)现在将创建三个表格至我们的数据库,用于我们的三个领域类,并在表格之间创建必要的关联。再次运行应用程序,并打开 DB 控制台以查看新的表格。

5.4 数据启动

每个 Grails 项目在 grails-app/init 下都会包含一个 BootStrap.groovy 文件。该文件可用于应用程序启动过程中您想要执行的任何自定义逻辑。该文件的一个很好用途是预加载数据库中的某些数据。让我们创建我们的三个领域类的几个实例。

编辑 grails-app/init/org/grails/guides/BootStrap.groovy,如下所示

grails-app/init/org/grails/guides/BootStrap.groovy
package org.grails.guides

import groovy.transform.CompileStatic

@CompileStatic
class BootStrap {

    MakeService makeService
    ModelService modelService
    VehicleService vehicleService
    def init = { servletContext ->

        Make nissan = makeService.save('Nissan')
        Make ford = makeService.save( 'Ford')

        Model titan = modelService.save('Titan', nissan)
        Model leaf = modelService.save('Leaf', nissan)
        Model windstar = modelService.save('Windstar', ford)

        vehicleService.save('Pickup', nissan, titan, 2012).save()
        vehicleService.save('Economy', nissan, leaf, 2014).save()
        vehicleService.save('Minivan', ford, windstar, 1990).save()
    }
    def destroy = {
    }
}

现在,重新启动应用程序并浏览到 DB 控制台,您应该能够展开三个表格并查看我们新创建的数据。

5.5 数据源

默认情况下,Grails 配置了一个内存中 H2 数据库,每次重新启动应用程序时都会将该数据库删除并重新创建。对于本指南的目的而言,这已经足够了,但是,您可以通过配置自己的数据源,轻松将其更改为本地数据库实例。我们将以 MySQL 为例。

5.6 配置 MySQL 数据源

编辑 build.gradle

build.gradle
dependencies {
    //...

    runtime 'mysql:mysql-connector-java:5.1.40' (1)
1 将 MySQL JDBC 驱动程序添加为依赖项
请务必将依赖项添加到 build.gradle 文件的 dependencies 部分,而不是 buildscript/dependencies 部分。前者是应用程序依赖项(在编译时、运行时或测试时需要),而后者是 Gradle 构建流程所需的依赖项(例如管理静态资产)。

编辑 application.yml

grails-app/conf/application.yml
dataSource:
    pooled: true
    jmxExport: true
    driverClassName: com.mysql.jdbc.Driver   (1)
    dialect: org.hibernate.dialect.MySQL5InnoDBDialect
    username: sa
    password: testing
environments:
    development:
        dataSource:
            dbCreate: update
            url: jdbc:mysql://127.0.0.1:3306/myapp  (2)
1 driverClassNamedialect 改为 MySQL 设置
2 这假设您有一个名为 myapp 的数据库的本地 MySQL 实例

5.7 Grails 控制台

现在,我们没有设置任何控制器或视图来使用我们的域模型。我们很快会做到,但目前,让我们启动 Grails 控制台,这样我们可以探索 Grails 和 GORM 提供的内容。

如果应用程序仍在运行,请用 kbd:[Ctrl+C] 将其关闭,或者(如果在 交互模式 下运行 Grails,则用 stop-app 命令)关闭它。

启动 Grails 控制台

$ ./grailsw console

Grails 控制台应用程序将启动。这个应用程序基于 Groovy 控制台,但附加的好处是我们的整个 Grails 应用程序在后台启动并运行,这样我们便可以从控制台访问我们的域类,甚至可以持久保存到数据库。

尝试从控制台使用我们的新域模型。下面有一个帮助您入门简单的脚本 - 此外,请参考 GORM 文档,以获取有关查询、持久性、配置等的更多详细信息。

docs/console.groovy
import org.grails.guides.*

def vehicles = Vehicle.list()

println vehicles.size()

def pickup = Vehicle.findByName("Pickup")

println pickup.name
println pickup.make.name
println pickup.model.name

def nissan = Make.findByName("Nissan")

def nissans = Vehicle.findAllByMake(nissan)

println nissans.size()

6 控制器

本部分将重点介绍创建控制器和定义操作的基本知识。

虽然不是“MVC”三元组的一部分,但 Grails 也支持 服务。在任何复杂性的 Grails 应用程序中,将核心应用程序逻辑保留在服务中被认为是最佳实践。我们将在本指南的后面部分介绍服务。

遵循约定优于配置的原则,Grails 会将 grails-app/controllers/ 下的任何 Groovy 类配置为控制器,而无需任何其他配置。您可以自己创建 Groovy 类,或使用 create-controller 命令来生成控制器和关联的测试规范。

$ ./grailsw create-controller org.grails.guides.Home

| Created grails-app/controllers/org/grails/guides/HomeController.groovy
| Created src/test/groovy/org/grails/guides/HomeControllerSpec.groovy
请注意,Grails 会自动添加 *Controller 后缀。

让我们看一下我们的新控制器。

grails-app/controllers/org/grails/guides/HomeController.groovy
package org.grails.guides

class HomeController {

    def index() { }
}

Grails 创建了一个具有单个操作的控制器。操作是控制器中的公共方法,可以响应请求。通常,控制器操作将接收一个请求,获取一些数据(如果存在,则需要使用请求的参数或主体),并将结果呈现到浏览器(例如,作为网页)。控制器操作还可以重定向请求、转发、调用服务方法和返回 HTTP 响应代码。有关控制器操作的更多信息,请参阅 Grails 文档

在此特定操作中,我们暂时不需要逻辑,但我们希望它呈现页面。我们将在 [Views] 部分更详细地介绍 GSP 页面,但现在,我们为我们的 HomeController.index 操作创建非常简单的 GSP 页面来显示。

grails-app/views/home 目录下创建 index.gsp 文件。

grails-app/views/home/index.gsp
<html>
<html>
<head>
    <meta name="layout" content="main"/>
    <title>Home Page</title>
</head>
<body>

<div id="content" role="main">
    <section class="row colset-2-its">
        <h1>Welcome to our Home Page!</h1>
    </section>
</div>

</body>
</html>

再次运行应用程序并浏览到 http:localhost:8080/home。您应该看到您的新页面。

根据约定,Grails 将控制器操作映射到具有相同名称的视图,位于 grails-app/views/[controller name] 目录中。您可以覆盖该名称并指定特定视图(或全部呈现其他内容)。

我们将在下一节更详细地介绍视图和 GSP,但现在,您应该注意我们的 index.gsp 文件基本上是一个 HTML 页面,包含几个不寻常的标签。可以任意修改此新的主页。

6.1 URL 映射

既然我们有了新的“主页”,那么最好将其作为应用程序的着陆页,而不是 Grails 默认页。为此,我们需要更改我们的 UrlMappings.groovy 文件。

Grails 使用 UrlMappings.groovy 文件将请求路由到正确的控制器和操作。它们可以与重定向到控制器和/或操作的 URI 字符串一样简单,也可以包含通配符和约束,并且变得非常复杂。

Grails 文档 中了解有关 URL 映射的更多信息

让我们看一下默认的 URLMappings.groovy 文件。

grails-app/controllers/org/grails/guides/UrlMappings.groovy
package org.grails.guides

class UrlMappings {

    static mappings = {
        "/$controller/$action?/$id?(.$format)?"{  (1)
            constraints {
                // apply constraints here
            }
        }

        "/"(view:"/index")   (2)
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}
1 Grails 默认 URL 映射 - 此规则导致根据名称将请求映射到控制器和操作(以及可选的 ID 和/或格式)。所以 home/index 将映射到 HomeControllerindex 操作。
2 此 URL 映射将根 URI (/) 指向特定视图。

让我们更改 / 规则以指向我们的新 HomeController。编辑该行如下:

grails-app/controllers/org/grails/guides/UrlMappings.groovy
package org.grails.guides

class UrlMappings {

    static mappings = {
//...

        "/"(controller:"home")   (1)
//...
    }
}
1 view: "/index" 更改为 controller: "home"

根据约定,请求控制器而不带操作名称将转到 index 操作(如果存在,否则将抛出错误)。如果需要,可以通过在控制器中指定 defaultAction 属性来更改此行为。

grails-app/controllers/org/grails/guides/HomeController.groovy
package org.grails.guides

class HomeController {

    static defaultAction = "homePage"

    def homePage() { } (1)
}
1 不要进行此更改,这只用于演示目的

现在您已将 / 规则更改为指向您的新 HomeController,如果重新启动应用程序并浏览到 http://localhost:8080,您应该会看到您的新的主页。

6.2 辅助工具

我们希望有操作来允许我们创建新域类实例并将它们持久保存到数据库中。此外,我们希望能够编辑现有实例甚至删除它们。通常,所有这些功能都需要大量编码,但通过辅助工具,Grails 为我们提供了一个良好的开端。

了解有关 Grails 文档 中脚手架的更多信息。

6.3 动态脚手架

现在我们已有一个主页,让我们创建控制器来管理我们之前创建的领域模型。为每个领域类创建 3 个新控制器,即 VehicleMakeModel

$ ./grailsw create-controller Vehicle

| Created grails-app/controllers/org/grails/guides/VehicleController.groovy
| Created src/test/groovy/org/grails/guides/VehicleControllerSpec.groovy

$ ./grailsw create-controller Make

| Created grails-app/controllers/org/grails/guides/MakeController.groovy
| Created src/test/groovy/org/grails/guides/MakeControllerSpec.groovy

$ ./grailsw create-controller Model

| Created grails-app/controllers/org/grails/guides/ModelController.groovy
| Created src/test/groovy/org/grails/guides/ModelControllerSpec.groovy

要使用脚手架,请编辑我们刚刚创建的这三个控制器,并将默认的 index 操作替换为以下示例中所示的 scaffolding 属性。

grails-app/controllers/org/grails/guides/VehicleController.groovy
package org.grails.guides

class VehicleController {

    static scaffold = Vehicle
}
grails-app/controllers/org/grails/guides/MakeControler.groovy
package org.grails.guides

class MakeControler {

    static scaffold = Make
}
grails-app/controllers/org/grails/guides/ModelController.groovy
package org.grails.guides

class ModelController {

    static scaffold = Model
}

设置 scaffold 属性后,Grails 现在会为各个领域类生成所有必需的 CRUD(创建、读取、更新、删除)操作。它还将使用我们的领域属性和关联动态生成带有列表、创建、展示和编辑页面的视图。当应用程序的开始阶段组合在一起时,这会对你大有帮助。

重新启动应用程序并浏览到 http://localhost:8080/vehicle - 你应该会看到我们添加到 BootStrap 中的 Vehicle 实例的列表。试用新视图,并创建、查看、编辑和删除一些实例。你还可以对 ModelMake 控制器执行相同的操作。

6.4 静态脚手架

动态脚手架功能强大,有时会提供所有你需要的所有功能(特别对于管理站点,其中数据访问比演示更重要)。但你很可能会感觉到需要自定义生成的视图和控制器,以便更改其外观或添加自定义逻辑和功能。Grails 预期了这种需求并提供了一组可以生成我们刚才看到的控制器和/或视图的 generate 命令,允许你根据需要修改它们。

要生成视图(并继续使用动态脚手架)

$ ./grailsw generate-views Vehicle

要生成控制器(并继续使用动态 GSP 视图)

$ ./grailsw generate-controller Vehicle

要生成视图和控制器(绕过所有动态生成)

$ ./grailsw generate-all Vehicle

生成的控制器将放在 grails-app/controller 下,生成的视图将放在 grails-app/views/vehicle 下。

要覆盖现有文件,请将 -force 标志与 generate-* 命令一起使用:./grailsw generate-all com.example.Vehicle -force

让我们为 Vehicle 生成控制器和视图,并了解一下生成控制器的结果。

$ ./grailsw generate-all Vehicle -force

grails-app/controllers/org/grails/guides/ 中打开 VehicleController.groovy 文件。

import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.CREATED
import org.grails.guides.Vehicle
import grails.gorm.transactions.Transactional

@SuppressWarnings(['LineLength'])
@ReadOnly (1)
class VehicleController {

    static namespace = 'scaffolding'

    static allowedMethods = [save: 'POST', update: 'PUT', delete: 'DELETE']

    def index(Integer max) {
        params.max = Math.min(max ?: 10, 100) (2)
        respond Vehicle.list(params), model:[vehicleCount: Vehicle.count()] (3)
    }

    def show(Vehicle vehicle) {
        respond vehicle (3)
    }

    @SuppressWarnings(['FactoryMethodName', 'GrailsMassAssignment'])
    def create() {
        respond new Vehicle(params) (3)
    }

    @Transactional (1)
    def save(Vehicle vehicle) {
        if (vehicle == null) {
            transactionStatus.setRollbackOnly()
            notFound()
            return
        }

        if (vehicle.hasErrors()) {
            transactionStatus.setRollbackOnly()
            respond vehicle.errors, view:'create'  (3)
            return
        }

        vehicle.save flush:true

        request.withFormat {  (4)
            form multipartForm {
                (5)
                flash.message = message(code: 'default.created.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id])
                redirect vehicle
            }
            '*' { respond vehicle, [status: CREATED] } (3)
        }
    }

    def edit(Vehicle vehicle) {
        respond vehicle (3)
    }

    @Transactional (1)
    def update(Vehicle vehicle) {
        if (vehicle == null) {
            transactionStatus.setRollbackOnly()
            notFound()
            return
        }

        if (vehicle.hasErrors()) {
            transactionStatus.setRollbackOnly()
            respond vehicle.errors, view:'edit' (3)
            return
        }

        vehicle.save flush:true

        request.withFormat {
            form multipartForm {
                (5)
                flash.message = message(code: 'default.updated.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id])
                redirect vehicle (6)
            }
            '*' { respond vehicle, [status: OK] } (3)
        }
    }

    @Transactional (1)
    def delete(Vehicle vehicle) {

        if (vehicle == null) {
            transactionStatus.setRollbackOnly()
            notFound()
            return
        }

        vehicle.delete flush:true

        request.withFormat {
            form multipartForm {
                (5)
                flash.message = message(code: 'default.deleted.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id])
                redirect action: 'index', method: 'GET' (6)
            }
            '*' { render status: NO_CONTENT } (7)
        }
    }

    protected void notFound() {
        request.withFormat {
            form multipartForm {
                (5)
                flash.message = message(code: 'default.not.found.message', args: [message(code: 'vehicle.label', default: 'Vehicle'), params.id])
1 @Transactional 注释配置控制器的交易行为或方法。交易用于管理持久性和应该一起完成的其他复杂操作(并且如果任何一个步骤失败,很可能会回滚)。有关事务的更多信息,请参阅 Grails 文档
2 params 对象可供所有控制器访问,它包含请求中的 URL 参数的一个表。您可以通过名称引用任何参数以检索值:params.myCustomParameter 将匹配此 URL 参数:[url]?myCustomParameter=hello。有关更多详细信息,请参阅 Grails 文档
3 respond 方法获取一个对象以返回给请求方,并通过 内容协商 来选择正确的类型(例如,请求的 Accept 标头可能指定 JSON 或 XML)。respond 还可以接受参数表,诸如 model(它用于定义该数据在页面中加载方式)。有关如何使用 respond 方法的更多信息,请参阅 Grails 文档
4 request 可供所有控制器访问,且是 Servlet API 的 HttpServletRequest 类的实例。您可以访问请求标头,将属性存储在请求范围内,并使用此对象获取有关请求方的信息。有关更多信息,请参阅 Grails 有关 request 的文档
5 flash 是一个映射,它会在会话中储存供下一个请求使用的对象,且会在下一个请求完成后自动清除它们。这对于传递错误消息或您希望下一个请求访问的其他数据非常有用。有关更多信息,请参阅 Grails 有关 flash 的文档
6 redirect 方法非常简单 - 它允许动作将请求重定向到另一个动作、控制器或 URI。您还可以将参数与重定向一起传递。有关 redirect 的更多信息,请参阅 Grails 文档
7 render 方法是 respond 的一个不太复杂版本 - 它不执行内容协商,因此您必须确切指明您想要渲染什么。您可以渲染纯文本、视图或模板、HTTP 响应代码或任何具有字符串表示形式的对象。请参阅 Grails 文档

这可真是一大段代码!生成和修改一个脚手架控制器是一个不错的学习练习,因此,请随意试验和修改此代码 - 您始终可以恢复到本指南的 completed 项目中的版本。

6.5 渲染一个响应

让我们修改 HomeController 以在我们的主页中渲染一些自定义内容。编辑 grails-app/controllers/org/grails/guides/HomeController.groovy

grails-app/controllers/org/grails/guides/HomeController.groovy
package org.grails.guides

class HomeController {

    def index() {
        respond([name: session.name ?: 'User', vehicleTotal: Vehicle.count()]) (1)
    }

    def updateName(String name) {
        session.name = name (2)

        flash.message = "Name has been updated" (3)

        redirect action: 'index' (4)
    }

}
1 我们调用 respond 方法来将内容的 Groovy 映射呈现给请求者,其中包括会话中的 name 属性(如果没有会话值,则默认为 “用户”)和 GORM 的 count 方法中的当前 Vehicle 实例总数。
2 session 是 Servlet API 的 `HttpSession` 类的实例,在所有控制器中可用。我们可以在会话中检索和存储属性 - 在这里,我们在会话中使用带有属性 nameString 存储。详情请参阅 Grails 文档
3 我们使用 flash 范围设置一个消息,并在下个请求中显示
4 我们没有任何特定内容要在此动作中显示,所以我们对 index 动作发出重定向(请注意,在 Groovy 方法中只要至少有一个参数,括号就是可选的)。

我们已更新 index 动作,以便将一些自定义内容呈现到页面,并且已创建一个新动作 updateName,它接受一个 String 参数,并将其保存在 session 中以供以后检索。不过,我们需要更新我们的视图以 1. 显示新可用的内容,2. 提供一些调用 updateName 和设置 session 属性的方法。

编辑 grails-app/views/home/index.gsp

grails-app/views/home/index.gsp
<html>
<html>
<head>
    <meta name="layout" content="main"/>
    <title>Home Page</title>
</head>
<body>

<div id="content" role="main">
    <section class="row colset-2-its">
        <h1>Welcome ${name}!</h1> (1)

        <h4>${flash.message}</h4>  (2)

        <p>There are ${vehicleTotal} vehicles in the database.</p> (1)

        <form action="/home/updateName" method="post" style="margin: 0 auto; width:320px"> (3)
            <input type="text" name="name" value="" id="name">
            <input type="submit" name="Update name" value="Update name" id="Update name">
        </form>

    </section>
</div>

</body>
</html>
1 我们可以在 GSP 页面中按名称引用我们 “model” 中的任何值,使用 Groovy 字符串表达式 ${name} ${vehicleTotal}
2 这里我们正访问我们的 flash.message 属性 - 如果它为 null,则不会在此处呈现任何内容。
3 这是一个简单的 HTML 表单,它会将 name 文本字段提交给刚创建的 updateName 动作。

运行该应用程序,你应会在 <h1> 页眉中看到我们的新消息:“欢迎用户!”,以及数据库中的当前 Vehicle 实例总数。

尝试在表单中输入自己的姓名并提交 - 你应会看到页面重新加载且自己的姓名替换了 “用户”。刷新页面几次。由于我们已将 name 存储在会话中,因此它将持续存在只要会话仍然有效。

内容协商

请记住,我们使用 respond 方法(而非更简单的 render)方法将我们的 “model” 发送到该页面。这意味着我们除了 HTML 页面以外还可以使用其他格式(例如 JSON 或 XML)来获取我们的 model。

在终端中运行以下命令(在应用程序运行时)

$ curl -i -H "Accept: application/json" "http://localhost:8080/home/index"

HTTP/1.1 200
X-Application-Context: application:development
Set-Cookie: JSESSIONID=008B45AAA1A820CE5C9FDC2741D345F3;path=/;HttpOnly
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 11 Jan 2017 04:06:57 GMT

{"name":"User","vehicleTotal":3}

我们已使用 curl 来调用我们的 index 动作,但已将我们 Accept 页眉更改为 application/json。现在,而不是一个 HTML 页面,我们以 JSON 接收相同数据。

得益于 Grails 的默认 URL 映射(显示如下),你也可以在浏览器中请求不同的内容类型

grails-app/controllers/org/grails/guides/UrlMappings.groovy
        "/$controller/$action?/$id?(.$format)?" {
            constraints {
                // apply constraints here
            }
        }

请注意映射中的 (.$format)? 令牌。这将匹配我们 URL 上的一个后缀,例如 .json.xml。在浏览器中对此进行测试。

浏览至http://localhost:8080/home/index.json。你应当会看到与通过 curl 获取的相同 JSON 正文。

尝试将 .json 改为 .xml。你应当会看到该模型的 XML 格式。内容协商允许你的控制器非常灵活,并为不同的客户端的相同操作返回合适的数据。

7 个视图

视图是 MVC 模式的第三个组成部分。视图负责向用户呈现数据(可能是浏览器页面、API 端点或某种其他消费者)。在许多应用中,视图都是 HTML 页面,设计为在浏览器中加载。但根据请求该视图的客户端的类型,将其“视图”设置为 XML 或 JSON 文档也完全合理。

Grails 的主视图技术是 Groovy Server Pages。它遵循了 JSP 和 ASP 的许多惯例,但自然基于 Groovy 语言。GSP 页面本质上是 HTML 文档,但支持许多特殊标签(通常以 g: 为前缀)以允许对你的视图进行编程控制。你甚至可以在 GSP 页面中编写任意 Groovy 代码,但这是强烈反对的 —— 理想情况下,GSP 页面应仅包含与视图相关逻辑和内容;任何类型数据操作或处理均应在渲染视图之前在控制器中(或服务中)发生。

在本指南中,你已使用 GSP 视图,但我们先快速概述一下基础知识。

布局

应用中的 GSP 视图常常需要共享一些通用结构,以及可能一些共享资产,如 JavaScript 文件。Grails 使用 SiteMesh 模版技术来支持“布局”的概念,其本质上是 GSP 页面可以“继承”的 GSP 模版文件。

根据惯例,布局位于 grails-app/views/layouts 下。Grails 在默认项目中包含一个 main.gsp 模版,这就是 Grails 支架使用的,也是默认主页。我们也在使用它。要使用 GSP 布局,只需使用 <meta name="layout"> 标签指定布局名称

<html>
<html>
<head>
    <meta name="layout" content="main"/>
</head>
<!-- ... -->

你还可以创建自己的布局。我们为我们的主页创建一个新布局.

$ vim grails-app/views/layouts/public.gsp

编辑你的新布局。我们开始会复制现有的 main.gsp,但我们会添加一个自定义徽标图像并移除我们的那些页面不需要的一些布局代码。

grails-app/views/layouts/public.gsp
<!doctype html>
<html lang="en" class="no-js">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>
        <g:layoutTitle default="Auto Catalog"/>
    </title>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>

    <asset:stylesheet src="application.css"/>

    <g:layoutHead/>
</head>
<body>

<div class="navbar navbar-default navbar-static-top" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="/#">
                <i class="fa grails-icon">
                    <asset:image src="logo.png"/>
                </i> Auto Catalog
            </a>
        </div>
        <div class="navbar-collapse collapse" aria-expanded="false" style="height: 0.8px;">
            <ul class="nav navbar-nav navbar-right">
                <g:pageProperty name="page.nav" />
            </ul>
        </div>
    </div>
</div>

<g:layoutBody/>

<div class="footer" role="contentinfo"></div>

</body>
</html>

此布局的关键点是 <g:layoutBody><g:layoutHead> 标签。这些标签是由 SiteMesh 代替使用该布局的任何 GSP 页面的 <head><body> 部分。

随时提供您自己的logo.png图像,或使用来自已完成项目的图像(或从该链接处下载)。将图像放置在grails-app/assets/images/目录中,布局应呈现该图像,而不是呈现 Grails 徽标。
不必担心新布局中的<asset>标记 - 我们将很快介绍它们。

现在编辑home/index.gsp视图以使用新public布局。

grails-app/views/home/index.gsp
<html>
<head>
    <meta name="layout" content="public"/> (1)
    <title>Home Page</title>
1 将“main”更改为“public”

刷新页面(或重新启动应用程序),您应当看到新布局已生效。如果您愿意,可以进一步修改public.gsp布局。

7.1 视图解析

Grails 如何选择需要呈现的视图?根据惯例,Grails 会在grails-app/views目录下查找视图。它将尝试通过将控制器名称与views目录下的目录匹配来将视图解析到控制器操作。例如,HomeController将解析为grails-app/views/home。然后,Grails 会将操作映射到具有相同名称的 GSP 页面。例如,index将解析为index.gsp

您还可以使用render方法从控制器操作呈现特定视图(覆盖 Grails 的惯例)。

class SomeController {
    def someAction() {
        render view: 'anotherView'
    }
}

这将尝试解析到grails-app/views/some/下的anotherView.gsp页面。如果您希望解析不在控制器自身视图目录下的视图,请使用前导/来指定从grails-app/views开始的绝对路径。

class SomeController {
    def someAction() {
        render view: '/another/view'
    }
}

这将解析到grails-app/views/another/下的view.gsp

7.2 GSP

GSP 页面可以访问丰富的标记集。我们已经看到了一些标记在发挥作用。您可以在 Grails 文档中获取有关可用 GSP 标记(包括如何定义自己的自定义标记)的更多详细信息。

让我们用一些 GSP 标记来对我们的index.gsp页面进行一些整理。

编辑 grails-app/views/home/index.gsp

grails-app/views/home/index.gsp
<%@ page import="Vehicle" %>
<html>
<head>
    <meta name="layout" content="public"/>
    <title>Home Page</title>
</head>
<body>

<div id="content" role="main">
    <section class="row colset-2-its">
        <h1>Welcome ${name}!</h1>
        <g:if test="${flash.message}"> (1)
            <div class="message" role="status">${flash.message}</div>
        </g:if>

        <p>There are ${vehicleTotal} vehicles in the database.</p>

        <ul>
        <g:each in="${Vehicle.list()}" var="vehicle">
            <li>
                <g:link controller="vehicle" action="show" id="${vehicle.id}">
                    ${vehicle.name} - ${vehicle.year} ${vehicle.make.name} ${vehicle.model.name}
                </g:link>
            </li>
        </g:each>
        </ul>

        <g:form action="updateName" style="margin: 0 auto; width:320px"> (2)
            <g:textField name="name" value="" />
            <g:submitButton name="Update name" />
        </g:form>

    </section>
</div>

</body>
</html>
1 无论是否存在flash.message,我们都使用<g:if>标记来测试是否存在信息,然后再呈现此消息(使用一些自定义样式)。
2 使用其 GSP 等效项替换简单的 HTML <form>标记。

让我们仔细看看<g:if>

    <g:if test="${isThisTrue}}>
        Some content
    </g:if>

GSP 标记可以选择接受属性,例如本例中的test。不同的标记需要不同类型的属性,但通常您最终会传递类似于本例中的 Groovy 表达式。${}之间的任何 Groovy 代码都将在(服务器上)进行计算,并且结果将在呈现的页面上替换。

您可以在 GSP 页面上的任何位置使用 Groovy 表达式,而不仅仅是在标记中。请参阅我们在index.gsp页面中的${flash.message}

其他标签属性可能会接受纯字符串或数字。例如,<g:form action="updateName">

GSP 标签还可以选择性地包含正文。如果 <g:if>test 表达式求值为真(遵循 Groovy Truth 惯例),则该正文将仅会被渲染。其他 GSP 标签(如 <g:form>)只是将正文包含在生成的 HTML 输出中。

7.3 GSP 标签迭代

迭代

GSP 也有用于迭代的标签,其中非常有用的一种是 <g:each>。让我们试一下

grails-app/views/home/index.gsp
<%@ page import="Vehicle" %> (1)
<html>
<!-- ... -->
        <p>There are ${vehicleTotal} vehicles in the database.</p>

        <ul>
            <g:each in="${Vehicle.list()}" var="vehicle">  (2)
                <li>
                    <! -- ... -->
                </li>
            </g:each>
        </ul>

<!-- ... -->

<g:each> 标签对由 in 属性提供的对象集合进行迭代。var 为集合中每个对象都设置一个名称。Grails 将对集合进行迭代(在这种情况下是通过 Vehicle.list() 返回的 Vehicles 列表),并为每个项渲染 <g:each> 标签的正文。

1 这是一个 JSP 风格的表达,允许执行任意 Groovy 代码(而不呈现结果)。我们在这里使用它来导入我们的 Vehicle 类。这是强烈不推荐的 - 我们稍后会解释原因。
2 不当做法,直接在视图中访问 Domain 模型
此类代码是一个坏主意 - 我们直接在我们的视图中访问了我们的域模型 (Vehicle),这将应用程序的两个独立部分紧密耦合在一起,而这通常会导致非常混乱的代码。实现此功能的更好方法是在 HomeController.index 操作中获取 Vehicle 列表,并将该列表添加到我们的模型对象(传递给 respond 的对象)中。然后,我们就可以像访问 namevehicleTotal 一样引用该列表。继续更改控制器和视图,以使用这种更好的方法 - 如果需要帮助,已完成的项目已经做了此更改。

让我们看看另一个常见的 GSP 标签:<g:link>

grails-app/views/home/index.gsp
        ${vehicle.name} - ${vehicle.year} ${vehicle.make.name} ${vehicle.model.name}
    </g:link>
</li>

<g:link> 渲染一个 HTML <a> 标签,但其优点在于它允许你按照 Grails 惯例指定你的链接目标,就像此示例(使用 controlleractionid 属性)一样。<g:link> 还足够智能,可以遵循我们的 URL 映射,因此,如果我们更改 vehicle/show 的 URL 映射,则 <g:link> 标签仍将呈现正确的 URL。<g:link> 支持许多其他属性,请参阅 Grails 文档 以了解更多内容。

7.5 资产管道

你可能已经注意到我们的 GSP 页面中有一些 <asset> 标签。这些标签是由 Asset Pipeline 插件提供的,这是 Grails 用于管理静态资产(图像、CSS、JavaScript 文件等)的默认工具。Asset Pipeline 插件提供了一组自定义的 GSP 标签,但与我们一直在探讨的标签不同,它使用 asset 前缀(或名称空间)。

最常见的 <asset> 标签如下所示

<asset:javascript src="myscript.js" /> (1)

<asset:image src="myimage.png" /> (2)

<asset:stylesheet src="mystyles.css" /> (3)
1 此标签从 grails-app/assets/javascripts 加载 JavaScript 文件
2 此标签从 grails-app/assets/images 加载图像
3 此标签从 grails-app/assets/stylesheets 加载 CSS 文件

如你所见,Asset Pipeline 采用根据惯例优先于配置的方式,遵循 Grails 的先例。但是,Asset Pipeline 是一个非常强大的框架,包括一个丰富的插件生态系统——你可以找到用于呈现 LESS 和 SASS 文件、CoffeeScript、Ember、Angular、JSX(React)的插件,等等。

Asset Pipeline 还支持你的资产的最小化和压缩,以及更多其他功能。

访问 asset-pipeline.com 以获取有关使用 Asset Pipeline 的更多信息,其中包括 可用插件 目录。

7.6 添加 Javascript 资产

让我们使用 Asset Pipeline 插件向我们的页面中添加 jQuery。Grails 默认包含 jQuery。本指南中使用的 Grails 版本默认包含 jQuery 2.2.0

_grails-app/assets/javascripts/jquery-2.2.0.min.js_

但是让我们下载最新版本。从 https://code.jqueryjs.cn/jquery-3.1.1.js 下载 jQuery

jquery-3.1.1.js 保存到 grails-app/assets/javascripts.

编辑 grails-app/views/home/index.gsp,在 head 块中添加以下代码片段。

grails-app/views/home/index.gsp
<asset:javascript src="jquery-3.1.1.js" />

<script type="text/javascript">
  $( document ).ready(function() {
    console.log( "jQuery 3.1.1 loaded!" );
  });
</script>

刷新页面,然后打开浏览器的开发者控制台。你应该在控制台日志中看到字符串 jQuery 3.1.1 loaded!.

8 服务

Grails 提供了一个“服务层”,由封装业务逻辑的类组成,并且使用 依赖注入 与应用上下文关联,以便任何控制器都可以注入并使用它们。服务是首选的应用程序逻辑媒介,而非控制器。

如果这看起来令人困惑,可以这样考虑:控制器用于响应请求并返回响应。服务可以在许多控制器(以及域类和来自其他服务)中重复使用。服务更通用,可以帮助你保持控制器简洁,并防止业务逻辑重复。通常,针对服务方法编写单元测试也比针对控制器操作更容易。

控制器用于“web 逻辑”,服务用于“业务逻辑”

同样按惯例,Grails 会将 grails-app/services 目录中的任何 Groovy 类配置为服务。服务将作为 Spring Bean 在 Grails 应用程序上下文中“关联”,这意味着你可以通过名称直接从任何其他 Spring Bean(包括控制器和域类)引用它们。

让我们添加一个功能,以生成基于制造商、型号和年份的 Vehicle 估计值。我们将把该逻辑放到一个服务中,然后从我们的应用程序代码调用它。

使用 create-service Grails 命令创建新服务

$ ./grailsw create-service ValueEstimateService

| Created grails-app/services/org/grails/guides/ValueEstimateService.groovy
| Created src/test/groovy/org/grails/guides/ValueEstimateServiceSpec.groovy

编辑 grails-app/services/org/grails/guides/ValueEstimateService.groovy

grails-app/services/org/grails/guides/ValueEstimateService.groovy
package org.grails.guides

import grails.gorm.transactions.Transactional

@Transactional
class ValueEstimateService {

    def serviceMethod() {

    }
}

Grails 已提供了一个 serviceMethod 存根作为示例。删除它并用以下内容替换

grails-app/services/org/grails/guides/ValueEstimateService.groovy
package org.grails.guides

import grails.gorm.transactions.Transactional

@Transactional
class ValueEstimateService {

    def getEstimate(Vehicle vehicle) {
        log.info 'Estimating vehicle value...'

        //TODO: Call third-party valuation API
        Math.round (vehicle.name.size() + vehicle.model.name.size() * vehicle.year) * 2
    }
}

显然,这种评估车辆价值的方法非常老套!实际上,您可能会调用第三方 Web 服务来进行评估,或者可能针对您自己的数据库运行查询。然而,此示例的目的是展示可以在服务中放置的“业务逻辑”类型,而不是在控制器或视图中进行计算。

现在,让我们使用我们的新服务。

编辑 grails-app/controllers/org/grails/guides/VehicleController.groovy(我们之前生成的脚手架控制器),并添加如下所示的属性

grails-app/controllers/org/grails/guides/VehicleController.groovy
static namespace = 'scaffolding'
1 只需在控制器中定义一个与服务类名称相同的属性,Grails 便会为我们注入该服务的一个引用。

现在(仍编辑 VehicleController.groovy),修改 show 操作,如下所示

grails-app/controllers/org/grails/guides/VehicleController.groovy
}

def show(Vehicle vehicle) {

我们已向 model 添加一个名为 estimatedValue 的新属性。此属性的值是我们调用我们的服务方法 getEstimate 的结果,我们将 vehicle 属性传递给该方法,即将要呈现该属性。

现在,在 show 页面上,我们可以访问 estimatedValue 属性并在页面上显示它。编辑 grails-app/views/vehicle/show.gsp,如下所示

grails-app/views/vehicle/show.gsp
<div id="show-vehicle" class="content scaffold-show" role="main">
    <h1><g:message code="default.show.label" args="[entityName]" /></h1>
    <h1>Estimated Value: <g:formatNumber number="${estimatedValue}" type="currency" currencyCode="USD" /></h1>

重新启动应用程序并浏览到 Vehicle 的显示页面,例如 http://localhost:8080/vehicle/show/1。您应该在页面上看到“估计值”

9 测试您的应用

测试是 Web 应用程序开发的重要组成部分。Grails 提供对三种测试类型(单元测试、集成测试和功能测试)的支持。单元测试 通常是最简单的类型,侧重于特定代码片断,而不依赖于应用程序的其他部分。集成测试 要求 Grails 环境启动并运行,并用于测试依赖于数据库或网络资源的功能。功能测试 要求该应用程序正在运行,并且旨在通过向其发出 HTTP 请求来练习该应用程序,就像用户会做的那样。这些往往是最复杂的测试。

Grails 使用的测试框架是 Spock。Spock 提供了一种基于 Groovy 语言的表达式语法来编写测试用例,因此非常适合 Grails。它包括一个 JUnit 运行器,这意味着 IDE 支持实际上是通用的(任何可以运行 JUnit 测试的 IDE 都可以运行 Spock 测试)。

Spock 是一个丰富的框架(即使在 Grails 应用程序之外也是如此),非常值得您花时间去掌握,如果您还没有这么做的话。查看 广泛的文档,以了解 Spock 的简介。

Grails 测试按惯例存储在 src/test/groovy 目录(单元测试)和 src/integration-test/groovy 目录(集成/功能测试)中。

您可以使用 test-app 命令运行 Grails 测试

$ ./grailsw test-app

如果您只想要运行单元测试或集成/功能测试,您可以在命令行标记中传递一条命令以选择其一。

$ ./grailsw test-app -unit
$ ./grailsw test-app -integration

您还可以通过将测试类作为参数传递来运行特定测试。

$ ./grailsw test-app org.grails.guides.MyTestSpec

编写测试是一个非常广泛的主题,值得自己编写指南。在实践中,最简单(通常也是最有用的)测试是单元测试,因此我们编写一个简单的单元测试来练习我们的 `ValueEstimateService`。

Grails 自动为使用 `create-service` 命令创建的服务创建测试规范。打开 `src/test/groovy/org/grails/guides/ValueEstimateServiceSpec`。

src/test/groovy/org/grails/guides/ValueEstimateServiceSpec
package org.grails.guides

import grails.testing.gorm.DataTest
import grails.testing.services.ServiceUnitTest
import spock.lang.Specification

class ValueEstimateServiceSpec extends Specification implements ServiceUnitTest<ValueEstimateService>, DataTest {

    def setup() {
    }

    def cleanup() {
    }

    void "test something"() {
        expect:"fix me"
            true == false
    }
}

当前我们的测试规范有一个测试,“测试某些内容”,它断言 `true == false`。Grails 乐于助人地通过开始一项失败的测试来鼓励你做正确的事情。

尝试运行测试,只是为了确认它失败了

$ /grailsw test-app org.grails.guides.ValueEstimateServiceSpec

...
> There were failing tests. See the report at: file:///Users/dev/projects/creating-your-first-grails-app/complete/build/reports/tests/test/index.html

BUILD FAILED

Total time: 6.353 secs
| Tests FAILED Test execution failed

现在我们已经确认我们的测试正在失败,我们编辑此测试用例来练习我们的 `getEstimate` 方法。编辑 `src/test/groovy/org/grails/guides/ValueEstimateServiceSpec`。

src/test/groovy/org/grails/guides/ValueEstimateServiceSpec.groovy
package org.grails.guides

import grails.testing.gorm.DataTest
import grails.testing.services.ServiceUnitTest
import spock.lang.Specification

class ValueEstimateServiceSpec extends Specification implements ServiceUnitTest<ValueEstimateService>, DataTest {

    void setupSpec() { (1)
        mockDomain Make
        mockDomain Model
        mockDomain Vehicle
    }

    void testEstimateRetrieval() {
        given: 'a vehicle'
        def make = new Make(name: 'Test')
        def model = new Model(name: 'Test', make: make)
        def vehicle = new Vehicle(year: 2000, make: make, model: model, name: 'Test Vehicle')

        when: 'service is called'
        def estimate = service.getEstimate(vehicle)

        then: 'a non-zero result is returned'
        estimate > 0

        and: 'estimate is not too large'
        estimate < 1000000
    }
}
1 在 Grails 3.3 中使用更新的测试框架模拟多个对象时,我们现在在安装过程中执行模拟,不再需要 @Mock 注释。

在此测试中,我们让事情变得非常简单,因为我们没有非常复杂的逻辑要测试,但这样您也可以专注于 Spock 测试用例的基本公式。Spock 提供了一组关键字,允许您以非常人性化的形式布置测试。

  • `given` 表示一个 “安装” 块- 在此处,您可以设置完成测试所需的任何对象或变量。

  • `when` 和 `then` 是 Spock 中最常见的 "配对" 之一(另一种未在此处使用的配对是 `expect`/`where` - 它们定义一个语句和一个预期结果。

  • `and` 只是继续当前 `then` 块,但允许您为多个断言指定期望。请注意,所有这些块都接受(可选地)字符串说明,这使您的测试更具可读性。例如,`when: “此方法被调用”`,`then: “期望此结果”`。

继续并重新运行此测试- 如果一切顺利,它应该以优异的成绩通过。

$ ./grailsw test-app org.grails.guides.ValueEstimateServiceSpec

...

BUILD SUCCESSFUL

| Tests PASSED

10 部署您的应用

开发 Grails 应用程序的最后一步是将完成的项目构建成可部署的包。通常,Java Web 应用程序作为 WAR 文件进行部署,而 Grails 通过 `war` 命令让它变得很容易。

$ ./grailsw war
...
BUILD SUCCESSFUL

| Built application to build/libs using environment: production

本指南并未介绍配置主题(尽管我们确实对我们的配置文件进行了一些编辑),但有必要在此提一下,Grails 支持“开发”、“测试”和“生产”等环境概念。每个环境都可以有其自己的配置属性和值,因此您可以在开发和生产系统之间进行不同的设置。默认情况下,war 命令使用“生产”环境,您可以使用如下所示的 -Dgrails.env 标志对此进行覆盖

$ ./grailsw war -Dgrails.env=development
...
BUILD SUCCESSFUL

| Built application to build/libs using environment: development

一旦我们有了 WAR 文件,就可以在任何 JEE 容器中部署它,例如 Tomcat

恭喜!您已经构建了您的第一个 Grails 应用程序。

11 您需要 Grails 提供的帮助吗?

Object Computing, Inc.(OCI)赞助了本指南的创建。提供各种咨询和支持服务。

Grails 的家园是 OCI

与团队会面