创建你的第一个Grails应用(中文翻译)

本文基于Zachary KleinCreating your first Grails Application

学习如何创建一个Grails应用,从无到有。

翻译进度完成度:

  • 汉化:完成
  • 校对文字:C9
  • 添加链接:C9
  • Windows/Mac 兼容
  • 增加code中文件名
  • 专有名词修订
  • 修正错误
  • 整理编号

(简易的)术语表:

  • profile 配置?
  • controller 控制器
  • domain
  • domain class 域类
  • model 模型
  • view 视图
  • scaffold 脚手架
  • transaction 事物
  • action 方法
    • 一般针对设计模式
  • method 方法
    • 一般针对函数
  • content negotiation 内容协商
  • layout 布局
  • render 渲染

注意一下,作者有时展示了完成一个功能的多种方法,所以强烈建议你在看完每一小段的教程之后再亲自动手。

Version:

grails -v
| Grails Version: 3.3.8
| Groovy Version: 2.4.15
| JVM Version: 1.8.0_202

1 让我们开始

在这篇文章中,你将要从无到有创建一个Grails应用程序。你将会学习有关DomainControllerServicesGSP单元、集成测试等相关知识。这篇文章是为了那些第一次接触Grails框架的人而撰写的。

1.1 你需要什么

为了完成这篇文章,你需要。

  • 一些时间
  • 一个可用的编辑器或IDE(这里首推ULTIMATE版IDEA)
  • 配置了环境变量的JDK1.8

1.2 如何完成这篇教程

这部分讲述了原作者建立的项目应该如何使用,但是在翻译的时候已经不太适用,所以此处的翻译略去,有兴趣的话可以访问原链接查看。

  • 配置Grails
  • Clone 项目

原教程项目文件

https://github.com/grails-guides/creating-your-first-grails-app

这个教程的项目有两个主要文件:

  • initial 存放初始项目
  • complete 存放完整版项目

2 创建Grails项目

正如以前提到过,创建新项目不能够再简单了。

只需运行

grails create-app myApp

注意到我们并没有指定具体的包名(package),这时package会默认使用当前app名称(e.g.,myapp)。

你也可以在grails-app/conf/application.yml中修改这个参数。

或者在创建的时候使用。

grails create-app org.grails.guides.myApp

2.1 安装Grails

由于此处的方法极多,请自行搜索教程。

2.2 Grails Application Forge

超出本教程要求,因此不做翻译。

Did you know you can download a complete Grails project without installing any additional tools? Go to start.grails.org and use the Grails Application Forge to generate your Grails project. You can choose your project type (Application or Plugin), pick a version of Grails, and choose a Profile - then click “Generate Project” to download a ZIP file. No Grails installation necessary!

You can even download your project from the command line using a HTTP tool like curl (see start.grails.org for API documentation):

curl -O start.grails.org/myapp.zip -d version=3.2.4 -d profile=angular

2.3 应用配置(Application Profile)

注意这部分内容你并不需要亲自动手操作,只需先了解即可,因为本教程不涉及profile的内容

您可以选择为Grails应用指定profile配置文件。配置文件适用于许多常见的应用程序类型,包括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配置文件。

3 运行项目

既然你已经创建了这个项目,我们不妨去尝试运行一下看看效果怎么样,看看有什么东西是Grails已经为我们提供好了的。

3.1 利用不同方式启动

3.1.1 利用grails命令运行

你可以用run-app命令来运行一个Grails项目。

grails run-app

3.1.2 利用grails wrapper命令运行

在[Grails Wrapper)](http://docs.grails.org/latest/guide/introduction.html#whatsNewGrailsWrapper的帮助下,在Grails 3.2.3以后的版本中,你也可以在不安装Grails的情况下运行项目。

./grailsw run-app # 使用wrapper

3.1.3 利用交互模式运行

你还可以利用Grails interactive mode来运行一个Grails runtime,你可以在交互模式中使用任何命令,而无需等待运行时为每个任务启动。

在本指南中,我们将更喜欢使用Grails wrapper

$ ./grailsw

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

3.1.4 使用Gradle运行

最后,由于Grails是基于Spring BootGradle构建的,你可以使用Spring Boot的命令来与你的Grails项目交互,比如bootRun

这些命令可用作Gradle任务。就像Grails本身一样,你无需在机器上安装Gradle。使用Gradle Wrapper(gradlew)时会自动下载

./gradlew bootRun

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

3.2 修改端口

如果你想修改监听的端口,只需在之前提到的application.yml中加入

# grails-app/conf/application.yml

server:
  port: 8090

一也可以在运行时直接指明端口。

./grailsw run-app --port=8090

3.3 热更新

现在的应用首页渲染的是含有应用相关信息的默认页面。这个默认页面位于grails-app/views/index.gsp

你可以尝试这查看并修改这个页面,比如

<!-- grails-app/views/index.gsp -->

<!-- Line 54 -->

<div id="content" role="main">
  <section class="row colset-2-its">
    <h1>Welcome to My Frist Grails Project</h1>
    <!-- 尝试修改h1标签中的文本 -->

保存你的修改,并且刷新浏览器中的页面。你将会立刻看到最新的修改已经被渲染到页面上了。Grailsviewscontrollersdomain classes以及其他的资源更新以后auto-reload,所以你不必刻意重启服务器。

有一大部分对于domain class的操作,比如重命名、修改关系等对应用Wiring的操作可能无法被auto-reload

4 Domain Classes

Grails是一个基于Spring Boot的,采用MVC架构的框架。典型的MVC架构的应用讲整个app划分为三个子项目。

  1. Model 定义和管理数据的代码
  2. View 管理代码如何呈现(如HTML)
  3. Controller 负责解决应用内部的逻辑,并且负责连接ModelViewController负责对请求做出响应,从Model中获取数据,并把它以特定的逻辑传给View

通常,面向对象的MVC框架要求开发人员配置哪些类对应于上述三个类别中的每一个。然而,Grails比大多数框架更进一步遵循“约定优于配置”的开发方法。这意味着对于Grails中的许多Artefact类型(ControllerView等),你只需在项目的特定目录中创建一个文件,Grails将自动将其连接到你的应用程序中,而无需你进行任何其他配置。

处理domain到数据库表(以及其他持久存储)的映射是GORMGrails Object Relational MapperGrails对象关系映射器)的工作。 GORMGrails框架中的一个强大工具,甚至可以在Grails项目之外独立使用。它支持关系数据库(通过Hibernate)以及MongoDbNeo4jRedisCassandra数据源。有关更多信息,请参阅GORM文档

当你构建一个MVC应用时,一般来说你要先从M入手——也就是domain model。在Grails中,你的domain将在grails-app/domainGroovy的类定义。

所以我们不妨先从Domain入手。

4.1 构建Domain Class

Domain class可以由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文件,一个是我们的Domain class,另一个是单元测试。让我们看看我们的Domain class是什么样的。

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

package org.grails.guides

class Vehicle {

    static constraints = {
    }
}

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

// grails-app/domain/org/grails/guides/Vehicle.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文档

4.2 DB Console

如果你再次运行该应用程序(注意一定要重新启动你的app),你应该看到与以前相同的页面。但是,你可以登录到数据库控制台并查看新的数据库表。

浏览到(http://localhost:8080/dbconsole)[http://localhost:8080/dbconsole]并登录。默认用户名是sa,没有密码。默认的JDBC URL是:

jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

你可以在/grails-app/conf/application.yml中查看JDBC url

登录到数据库控制台后,你应该会在左侧边栏中看到新的VEHICLES表。单击+图标展开表格 - 你应该看到列的列表,包括我们刚刚定义的三个字符串字段,名称,品牌和型号。

4.3 扩展Domain Model

相比你也能注意到,对于我们的Vehicle来说,它的makemodel字段目前还是String类型——这是很不合理的。因为modelmake都应该是相互关联的。(正如关系型数据库一样)所以让我们把Domain model扩展一下,变得更robust吧。

创建一下两个新的domain classes

$ ./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文档。 你可以回想一下实体之间的关系(1:n,1:1,n:m)

现在,更新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)现在将在我们的数据库中为我们的三个domain class创建三个表,并在表之间创建必要的关联。

再次运行应用程序(重启)并打开数据库控制台以查看新表。

4.4 Bootstrapping Data (初始化数据)

每个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

class BootStrap {

    def init = { servletContext ->

        def nissan = new Make(name: 'Nissan').save()
        def ford = new Make(name: 'Ford').save()

        def titan = new Model(name: 'Titan', make: nissan).save()
        def leaf = new Model(name: 'Leaf', make: nissan).save()
        def windstar = new Model(name: 'Windstar', make: ford).save()

        new Vehicle(name: 'Pickup',  make: nissan, model: titan, year: 2012).save()
        new Vehicle(name: 'Economy', make: nissan, model: leaf, year: 2014).save()
        new Vehicle(name: 'Minivan', make: ford, model: windstar, year: 1990).save()
    }
    def destroy = {
    }
}

现在重新启动应用程序,并浏览DBConsole,你应该能够展开这三个表并查看我们新创建的数据。

4.5 数据源(Datasources)

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

4.6 配置MySQL数据源

前提是你有MySQL数据库,不然译者比较建议你跳过这一段。

编辑build.gradle

// build.gradle

dependencies {
    //...

    runtime 'mysql:mysql-connector-java:5.1.40'

添加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. 将driverClassName和dialect更改为MySQL设置
  2. 这假设你有一个名为myapp的数据库的本地MySQL实例

4.7 Grails Console

现在我们没有设置任何控制器或view来使用我们的domain class。我们很快就会到达那一步,但是现在,让我们启动Grails console,以便我们可以探索GrailsGORM提供的内容。

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

启动Grails控制台:

$ ./grailsw console

Grails控制台应用程序将会启动。这个应用程序基于Groovy控制台,但具有额外的好处,即我们的整个Grails应用程序在后台启动并运行,因此我们可以访问我们的domain,甚至可以从控制台持久保存到数据库。

尝试从控制台使用我们的新domain class。这是一个简单的脚本,可以帮助你入门——再次参考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()

5 Controller

本节将重点介绍创建controller和定义操作的基础知识。

虽然不是“MVC”三角形的一部分,但Grails也提供对service的支持。在任何复杂的Grails应用程序中,将核心应用程序逻辑保留在service中被认为是最佳实践。我们稍后将在本指南中介绍它。

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

$ ./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后缀。

我们来看看我们的新的Controller

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

package org.grails.guides

class HomeController {

    def index() { }
}

Grails创建了一个具有单个action的控制器。action是控制器中的公共方法,可以响应请求。

通常,控制器动作将接收请求,获得一些数据(可选地使用参数或请求的主体,如果存在),并将结果呈现给浏览器(例如,作为网页)。

控制器操作还可以重定向请求,转发,调用服务方法以及返回HTTP响应代码。有关控制器操作的更多信息,请参阅Grails文档

我们尚未对此action中的逻辑有任何需求,但我们希望它能够呈现页面。我们将在view部分中更详细地查看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/[controllername]目录中具有相同名称的视图。你可以覆盖它并指定特定视图(或完全呈现不同的内容)。

我们将在下一节中更详细地介绍视图和GSP,但是现在,你应该注意我们的index.gsp文件基本上是一个HTML页面,带有几个不常见的标记。你可以根据需要随意修改这个新的主页。

5.1 URL Mappings (映射)

现在我们有了新的“主页”页面,如果它是应用程序的登录页面而不是Grails默认页面会很好。为此,我们需要更改我们的UrlMappings.groovy文件。

Grails使用UrlMappings.groovy文件将请求路由到适当的Controlleraction。它们可以像重定向到controller和/或action的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映射-此规则使请求根据名称映射到controller和操作(以及可选的ID和/或格式)。所以home/index将映射到HomeController,名为indexaction
  2. 此URL映射将根URI(/)指向特定视图。

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

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

package org.grails.guides

class UrlMappings {

    static mappings = {
//...

        "/"(controller:"home")  // 1.
//...
    }
}

  1. Change view: “/index” to controller: “home”

按照惯例,对没有操作名称的controller的请求将转到索引操作(如果存在)(如果不存在,将抛出错误)。如果需要,可以通过在controller中指定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,则应显示新的主页。

5.2 Scaffolding (脚手架)

我们希望有一些操作允许我们创建新的domain class实例并将它们保存到数据库中。此外,我们希望能够编辑现有实例甚至删除它们。通常所有这些功能都需要大量编码,但Grails为我们提供了scaffolding的来快速生成这些重复的代码。

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

5.3 Dynamic Scaffolding (动态脚手架)

现在我们有了一个主页,让我们创建控制器来管理我们之前创建的domain class。为每个domain class(Vehicle,Make和Model)创建3个新控制器。

$ ./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

要使用scaffolding,请编辑我们刚刚创建的三个控制器,并使用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(创建,读取,更新,删除)操作。它还将使用我们的域属性和关联(domain properties and associations)动态生成包含列表,创建,显示和编辑页面的视图。在一开始搭建应用程序时,这可以为你提供一个极大的便利。

重启应用程序,并浏览到http://localhost:8080/vehicle - 你应该看到我们添加到BootStrapVehicle实例列表。尝试新视图并创建,查看,编辑和删除某些实例。你也可以使用Model和Make控制器执行相同的操作。

5.4 Static Scaffolding

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

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

$ ./grailsw generate-views Vehicle

你要生成的部分是静态的scaffold,没有生成会继续使用动态scaffold

要生成控制器(并继续使用动态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文件。

注意由于版本问题,下述代码可能会与你的实际代码有细微区别,不过这并无大碍,让我们抓大放小。

// 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.transaction.Transactional
@SuppressWarnings(['LineLength'])
@Transactional(readOnly = true) // 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])
                redirect action: 'index', method: 'GET' //6.
  1. @Transactional 注解配置了控制器或方法的事务行为。事务用于管理持久性和应该一起完成的其他复杂操作(如果任何一个步骤失败,可能会回滚)。有关事务的更多信息,请参阅Grails文档
  2. params对象可供所有控制器使用,并包含请求中任何URL参数的映射。你可以按名称引用任何参数来检索值:params.myCustomParameter将匹配此URL参数:[url]?myCustomParameter=hello。有关更多详细信息,请参阅Grails文档
  3. respond方法把对象返回给请求者,使用content negotiation(内容协商)来选择正确的类型(例如,请求的Accept头可能指定JSON或XML)。respond也可以接受参数映射,例如model(定义数据在页面上加载的方式)。有关如何使用该respond方法的更多信息,请参阅Grails文档
  4. request在所有控制器上都可用,它是Servlet APIHttpServletRequest类的一个实例。你可以访问请求标头,在请求范围中存储属性,并使用此对象获取有关请求者的信息。有关更多信息,请参阅Grails文档
  5. flash是一个映射,用于存储会话中的对象以用于下一个请求,在下一个请求完成后自动清除它们。这对于传递你希望下一个请求访问的错误消息或其他数据非常有用。有关更多信息,请参阅Grails文档flash
  6. redirect方法很简单 - 它允许操作将请求重定向到另一个操作,控制器或URI。你还可以使用重定向传递参数。有关更多信息,请参阅Grails文档redirect
  7. render方法是一个不太复杂的版本respond——它不执行内容协商,因此你必须准确指定要呈现的内容。你可以呈现纯文本,视图或模板,HTTP响应代码或具有String表示形式的任何对象。请参阅Grails文档

这一部分有很多代码!

生成和修改脚手架控制器是一个很好的学习练习,因此可以随意尝试和修改此代码——你始终可以恢复到completed本指南的项目中的版本。(这里指的是英文版的github中的项目)

5.5 Render a response

让我们修改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内容映射render a Groovy map of content to the requestor,其中包含 1.会话中的属性name(如果不存在会话值,则默认为“User”)以及 2. GORM的count方法返回的,当前Vehicle的实例总数。
  2. sessionServlet APIHttpSession类的一个实例,并且在每个控制器中都可用。我们可以在会话中检索和存储属性——在这种情况下,我们将在会话中存储String类型的属性name。有关更多信息,请参阅Grails文档
  3. 我们使用flash语句来设置在下一个请求时显示的消息
  4. 我们没有在此操作中显示任何特定内容的需求,因此我们向index操作发出redirect重定向(请注意,只要存在至少一个参数,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><html>
  1. 我们可以使用Groovy String Expressions ${name} ${vehicleTotal}在GSP页面中按名称引用我们的“模型”中的任何值。
  2. 在这里,我们访问我们的flash.message属性——如果它为null,则此处不会呈现任何内容。
  3. 这是一个纯HTML表单,它将名称文本字段提交给我们刚刚创建的updateName操作。

运行应用程序,你应该在 标题中看到我们的新消息:"Welcome User!",以及数据库中当前的Vehicle实例总数。

尝试在表单中输入你自己的名称并提交——你应该看到页面重新加载,你自己的名称将替换”User”,刷新页面几次。因为我们将名称存储在会话中,所以只要会话有效,它就会一直存在。

5.6 Content Negotiation

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

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

$ 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来调用我们的索引操作,但是我们已经将Accept标头更改为application/json。现在我们在JSON中收到相同的数据,而不是HTML页面。

由于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表示。content negotiation会让你的控制器变得非常通用,并通过相同的操作将适当的数据返回给不同的客户端。

6 Views

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

Grails的主要视图技术是Groovy Server Pages。它遵循JSP和ASP的许多约定,但自然它基于Groovy语言。 GSP页面本质上是HTML文档,但它们支持许多特殊标记(通常以g:)作为前缀,以允许对你的视图进行编程控制。你甚至可以在GSP页面中编写任意Groovy代码,但强烈建议不要这样做 - 理想情况下,GSP页面应该只包含与视图相关的逻辑和内容;在呈现视图之前,控制器(或服务)中应该已经将任何类型的数据操作或处理完成。

你已经在本指南中使用了GSP视图,但让我们快速介绍一下基础知识。

Layouts

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

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

<!-- grails-app/views/layouts/main.gsp -->

<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视图以使用新的公共布局。

<!-- home/index.gsp -->

<html>
<head>
    <meta name="layout" content="public"/> <!-- 1. -->
    <title>Home Page</title>
  1. "main"换为"public"

刷新页面(或重新启动应用程序),你应该可以立即看到新的布局。如果你愿意,可以随意进一步修改public.gsp布局。

6.1 Views Resolution

Grails是如何知道要要选择哪个视图去渲染呢?按照惯例,Grails在grails-app/views目录下查找视图。它将尝试通过将控制器名称与views目录下的目录进行匹配来解析对控制器操作的视图。例如,HomeController将解析为grails-app/views/home。然后,Grails会将操作映射到具有相同名称的GSP页面。例如,索引将解析为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

6.2 GSP

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

让我们在index.gsp页面上添加一些GSPtags

编辑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. 我们使用<g:if>标签测试是否有message,然后渲染message(使用一些自定义样式),而不是一味地渲染flash.message而不管它是否存在。
  2. 将纯HTML<form>标记替换为其GSP等效项。

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

<!-- grails-app/views/home/index.gsp -->

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

GSP tag可以接受attributes(属性),例如本例中的测试。不同的标签需要不同类型的属性,但通常你最终会像本示例中那样传递Groovy Expression。将评估${}之间的任何Groovy代码(在服务器上),结果将在呈现的页面上替换。

你可以在GSP页面的任何位置使用Groovy Expressions,而不仅仅是在标签中。可以参考index.gsp页面中的${flash.message}

其他标记属性可能接受普通字符串或数字。例如,<g:form action="“updateName”">

GSP tag也可以选择包括一个主体。在<g:if>的情况下,只有在test表达式求值为true时才会呈现正文(遵循Groovy Truth约定)。其他GSP标记(如<g:form>)只是在生成的HTML输出中包含正文。

6.3 GSP Tags Iteration

6.3.1 Iteration

还有用于迭代的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()返回的Vehicle列表),并为每个项目呈现<g:each>标记的主体。

  1. 这是一个JSP样式的表达式,允许执行任意Groovy代码(而不会渲染结果)。我们在这里使用它来导入我们的Vehicle类。然而这种方式是非常令人沮丧的——我们很快就会解释原因。
  2. 不好的做法,直接从视图访问domain class

这种代码是一个坏主意 - 我们直接从我们的视图访问我们的domain class(Vehicle),它紧密地耦合应用程序的两个独立部分,并且通常导致非常混乱的代码。完成此功能的更好方法是在HomeController.index操作中获取Vehicle列表,并将列表添加到我们的model object(the one being passed to respond(传递给响应的对象))。然后我们可以像访问namevehicleTotal那样引用列表。继续更改控制器和视图以使用这种更好的方法 - 如果你需要帮助,已完成的项目已经进行了此更改。

我们来看一个更常见的GSP标记:<g:link>s

<!-- grails-app/views/home/index.gsp -->

<li>
    <g:link controller="vehicle" action="show" id="${vehicle.id}">
        ${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文档

6.5 Asset Pipeline

你可能已经注意到我们的GSP页面中有一些标签。这些标签由`Asset Pipeline`插件提供,这是Grails用于管理`static assets`静态资源(图像,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是一个非常强大的框架,包含一个丰富的插件生态系统 - 你可以找到插件来渲染LESSSASS文件,CoffeeScriptEmberAngularJSX(React)等等。

Asset Pipeline还支持缩小和压缩你的资源等等。

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

6.6 Add Javascript Asset

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

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

但是让我们下载最新版本。从https://code.jquery.com/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!在控制台日志中。

7 Services

Grails提供了一个“service layer服务层”,它们是封装业务逻辑的类,并且是有线的(使用dependency injection 依赖注入)连接到应用程序上下文中,因此任何控制器都可以注入和使用它们。服务是大多数应用逻辑的首选工具,而不是控制器。

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

控制器用于“Web逻辑”,service用于“业务逻辑”。

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

让我们添加一个功能,根据品牌,型号和年份生成Vehicle的估算值。我们将这个逻辑放在一个服务中,并从我们的应用程序代码中调用它。

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

$ ./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.transaction.Transactional

@Transactional
class ValueEstimateService {

    def serviceMethod() {

    }
}

Grails提供了一个serviceMethod作为示例。删除它并将其替换为以下内容:

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

package org.grails.guides

import grails.transaction.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

import static org.springframework.http.HttpStatus.CREATED
import org.grails.guides.Vehicle
import grails.transaction.Transactional

@SuppressWarnings('LineLength')
@Transactional(readOnly = true)
class VehicleController {

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

    def valueEstimateService // 1.

    ...
}
  1. 通过简单地在控制器中定义一个与我们的服务类同名的属性,Grails将为我们注入对服务的引用。

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

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

def show(Vehicle vehicle) {
        respond vehicle, model: [estimatedValue: valueEstimateService.getEstimate(vehicle)]
}

我们在model中添加了新属性,名为estimatedValue。这个属性的值是调用我们的服务方法getEstimate的结果,我们将传递我们要渲染的车辆属性。

现在,在显示页面上,我们可以访问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>   <!-- 1. -->

  1. <g:formatnumber>是另一个GSP标记,它为渲染数字提供了许多有用的选项,包括货币和小数精度。有关更多信息,请参阅Grails文档。

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

8 Testing your App

测试是Web应用程序开发的重要部分。 Grails为三种类型的测试提供支持:单元测试,集成测试和功能测试。

单元测试通常是最简单的一种,专注于特定的代码而不依赖于应用程序的其他部分。

集成测试要求Grails环境启动并运行,并用于测试依赖于数据库或网络资源的功能。

功能测试要求应用程序运行,并且旨在通过对其发出HTTP请求,几乎以用户的身份运行应用程序。这些往往是最复杂的测试。

Grails使用的测试框架是Spock

Spock提供了一种基于Groovy语言编写测试用例的富有表现力的语法,因此非常适合Grails。它包括一个JUnit runner,这意味着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
    }
}

目前我们的测试规范有一个测试,"test something",断言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 表示setup语句 —— 你可以在此处设置完成测试所需的任何对象或变量。
  • whenthen 是Spock中最常见的“一对儿”之一(另一个,这里没有使用,是expect/where —— 它们定义了一个声明和一个预期的结果。
  • and继续当前的then语句,但它允许你指定你对多个断言的期望。请注意,所有这些块都接受(可选)字符串描述,这使你的测试更具可读性。例如,when: "this method is called", then: "expect this result"

继续并重新运行此测试 - 如果一切顺利,它应该通过测试,并伴随一个小旗子。

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

...

BUILD SUCCESSFUL

| Tests PASSED

9 Deploying your App

开发Grails应用程序的最后一步是将完成的项目构建为可部署的包。通常,Java Web应用程序被部署为WAR文件,而Grails使用war命令可以轻松实现:

$ ./grailsw war
...
BUILD SUCCESSFUL

| Built application to build/libs using environment: production

我们没有在本指南中涉及configuration的相关主题(虽然我们对配置文件进行了一些编辑),但是值得一提的是Grails支持环境概念,例如“开发”,“测试”和“生产”。每个环境都可以拥有自己的配置属性和值,因此你可以在开发和生产系统之间进行不同的设置。默认情况下,war命令使用“生产”环境 - 你可以使用-Dgrails.env标志覆盖它,如下所示:

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

| Built application to build/libs using environment: development

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

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