显示导航

Grails 网址映射

学习如何配置路由和端点

作者: Zachary Klein

Grails 版本 5.0.1

1 开始

无论你正在构建一个传统的基于页面的应用程序、单页应用程序,还是微服务,拥有一个逻辑且可扩展的 API 是应用程序设计中非常重要的部分。Grails 为构建 API 提供了有力的支持,其支持使用网址映射

我们要构建的示例应用程序是一个会议安排应用程序,名为 agenda。我们将有一个简单的领域模型,包括演讲者演讲议程(将演讲者和演讲联系在一起)以及会议(有一个议程)。我们不会在本指南中创建领域模型,但我们将为它创建网络层。

1.1 你需要的

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

  • 一些时间

  • 一个合适的文本编辑器或 IDE

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

1.2 如何完成指南

要开始,请执行以下操作

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是一个带有附加代码以让你更进一步的简单 Grails 应用程序。

  • complete 已完成的示例。这是通过完成指南中介绍的步骤并将这些更改应用于 initial 文件夹的结果。

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

  • cdgrails-guides/grails_url_mappings/initial

然后按照下一章节的说明进行操作。

如果你cdgrails-guides/grails_url_mappings/complete,你可以直接进入完整示例

2 创建我们的应用程序

请从以下选项中选择一个来创建你的项目

1. 通过 Grails CLI 创建新的应用程序

如果你已安装 Grails(>= 3.3.0),你可以在命令行创建你的应用程序

~ grails create-app agenda

2. 通过 Application Forge 创建新的应用程序

你可以从 http://start.grails.org 下载你的项目,可以使用在线向导或以下 curl 命令

~ curl -O start.grails.org/agenda.zip -d version=3.3.2

3. 使用该指南库中的初始项目

在指南目录内,将 initial 项目复制到你选择的目录,对其重命名(例如,agenda),然后在该项目中按照指南执行操作。

3 创建控制器

在定义 URL 映射之前,我们需要创建一些控制器。你可以将它们视为我们应用程序的“Web 层”。控制器响应请求(即,来自用户浏览器或另一个微服务)、从模型获取数据并返回响应,例如 HTML 页面或 JSON 字符串。

你可以通过在 grails-app/controllers 目录下创建一个 Groovy 类来创建一个控制器。但是,Grails 提供了一个在命令行创建控制器的方便命令。

因为我们要创建几个控制器,让我们使用 Grails CLI 提供的交互模式。在你的项目目录中运行以下命令

~ ./grailsw
...

grails >

交互式 CLI 加载后,你可以运行任何可用的 Grails 命令,包括 create-controller。运行以下命令

grails > create-controller demo.Talk
grails > create-controller demo.Speaker
grails > create-controller demo.Agenda
grails > create-controller demo.Conference

继续并退出 Grails CLI

grails > exit

现在我们有了控制器,是时候创建将作为我们应用程序用户(无论是人类还是我们 API 的客户端)的端点公开的操作了。操作只是在控制器类中定义的方法。在CRUD(*C*reate、*R*ead、*U*pdate 和 *D*elete)控制器中,你通常需要如下所示的操作

class CrudController {

    def index() {} //usually a "list" view

    def save() {} //save new records

    def show(Serializable id) {} //retrieve details on a specific record

    def update(Serializable id) {} //update a specific record

    def delete(Serializable id) {} //delete specific record
}

如果这是一个REST控制器,则以上操作可能足以用于 CRUD 操作。如果我们还想为编辑和创建记录显示 HTML 表单,我们还可以添加 createedit 操作来显示这些页面。

让我们为我们的(未来)域模型构建 CRUD 操作。按如下所示编辑前三个控制器(TalkControllerSpeakerControllerAgendaController

grails-app/controllers/demo/TalkController.groovy
package demo

class TalkController {

    def index() {
        render "Retrieving all Talks..."
    }

    def save() {
        render "Saving new Talk..."
    }

    def show(Long id) {
        render "Retrieving Talk ${id}..."
    }

    def update(Long id) {
        render "Updating Talk ${id}..."
    }

    def delete(Long id) {
        render "Deleting Talk ${id}..."
    }
}
grails-app/controllers/demo/SpeakerController.groovy
package demo

class SpeakerController {

    def index() {
        render "Retrieving all Speakers..."
    }

    def save() {
        render "Saving new Speaker..."
    }

    def show(Long id) {
        render "Retrieving Speaker ${id}..."
    }

    def update(Long id) {
        render "Updating Speaker ${id}..."
    }

    def delete(Long id) {
        render "Deleting Speaker ${id}..."
    }
}
grails-app/controllers/demo/AgendaController.groovy
package demo

class AgendaController {

    def index() {
        render "Retrieving all Agendas..."
    }

    def save() {
        render "Saving new Agenda..."
    }

    def show(Long id) {
        render "Retrieving Agenda ${id}..."
    }

    def update(Long id) {
        render "Updating Agenda ${id}..."
    }

    def delete(Long id) {
        render "Deleting Agenda ${id}..."
    }
}

对于 ConferenceController,我们将创建一组不同的操作 - talksspeakersagenda。此外,talksspeakers 操作将接受可选的 id 参数,以便我们可以显示特定的演讲者或演讲。

按如下所示编辑 ConferenceController.groovy

grails-app/controllers/demo/ConferenceController.groovy
package demo

class ConferenceController {

    def talks(Long id) {
        if(id) {
            render "Returning conference talk ${id}..."
        } else {
            render "Returning conference talks..."
        }

    }

    def speakers(Long id) {
        if(id) {
            render "Returning conference speaker ${id}..."
        } else {
            render "Returning conference speakers..."
        }
    }

    def agenda() {
        render "Returning conference agenda..."
    }
}

现在,我们已经完成了控制器操作的存根,让我们看看 Grails URL 映射已经帮我们准备好了什么。

4 定义 URL 映射

让我们来看看默认的 UrlMappings.groovy 文件(位于 grails-app/controllers/demo 下)

package demo

class UrlMappings {

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

        "/"(view:"/index")
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}

mappings 块中的第一个映射非常简单。它有两个组件:一个字符串,用于定义 URL 模式,以及一个闭包,可在其中指定选项(例如,约束)以更改映射的行为。

请参阅 Grails 文档中 此处有关如何使用 URL 约束的说明。

URL 模式可以包含变量,这些变量由模式内的 Groovy 字符串表示(例如,$controller${controller})。Grails 有几个特殊的通配符,可以开箱即用。例如,$controller 将映射到给定控制器的名称,$action 将映射到该控制器中的一个操作,等等。可选变量后跟 ?。例如,/controller/$action?

对 URL 模式的所有部分使用变量意味着此特定映射将匹配任何 URL。请参见下表

表 1. URL 映射解析
URL 结果

/talk/show/1

TalkControllershow 操作,id 参数为 1

/talk/

TalkController,默认操作(通常为`index` 操作)

/speaker/show/1.json

SpeakerControllershow 操作,id 参数为 1format 参数为 json(可用于内容协商)

除了映射到控制器操作之外,URL 映射还可以直接映射到视图(而不指定控制器或操作),或任意的 URI(适用于重定向)。例如,根上下文的默认 URL 映射,/,指向 grails-app/views 下的默认 index.gsp 页面

    "/"(view:"/index")

您可以使用 uri 参数映射到不同的 URI

    "/hello"(uri: "/hello.dispatch")

另一种有用的映射类型是 HTTP 错误代码映射,在默认的 UrlMappings.groovy 文件中也可以看到。默认映射只加载一个默认的错误视图,但是你可以轻松地将这些请求重定向到一个用来应用自定义错误处理逻辑和在应用中恢复的错误处理或日志控制器中。例如,

    "500"(controller:'myErrorController', action: 'somethingBroke')
    "404"(controller:'myErrorController', action: 'notFound')

默认 URL 映射将涵盖本示例应用中我们的大部分需求。但是,为了让我们获得更细粒度的控制,我们将为上面创建存根的 ConferenceController 操作添加一些我们自己的映射。

默认的一组 URL 映射涵盖了许多用例。但是,当需要保护应用时,您可能希望移除默认映射,并用更具体的映射替换(这样用户无法通过仅仅弄清楚名称来访问任何他们想要的控制器操作)。

如下所示编辑 UrlMappings.groovy 文件

grails-app/controllers/demo/UrlMappings.groovy
package demo

class UrlMappings {

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


        "/talks/$id?"(controller: 'conference', action: 'talks') (1)

        "/speakers/$id?"(controller: 'conference', action: 'speakers')

        "/agenda"(controller: 'conference', action: 'agenda') (2)


        "/"(view:"/index")
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}
1 将自定义 URL 模式的 /talks/$id? 映射到 ConferenceController 操作。$id 变量代表 URL 模式的组件将映射到 talks 操作的 id 参数。后缀 ? 表示变量是可选的。
2 注意,我们并未在这种模式中使用任何变量,因此请求必须与 URL 模式完全匹配。否则,该模式将在文件顶部的默认 URL 映射中处理。

如果您现在启动应用程序并向 http://localhost:8080/talks 发送请求,您应该会看到文本显示在浏览器中

    Returning conference talks...

尝试其他 URL:http://localhost:8080/talks/1

    Returning conference talk 1...

HTTP 方法

在继续了解之前,还需要注意 URL 映射中的另一项功能。除了指定 URL 模式和约束外,映射还可以指定匹配该映射所需的特定 HTTP 方法。它的做法很简单,即使用指定的 HTTP 动词对模式进行前缀,例如 getpostput

让我们再次更新 UrlMappings.groovy 文件,指定仅接受请求的 GETConferenceController 操作。

grails-app/controllers/demo/UrlMappings.groovy
package demo

class UrlMappings {

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

        get "/talks/$id?"(controller: 'conference', action: 'talks')
        get "/speakers/$id?"(controller: 'conference', action: 'speakers')
        get "/agenda"(controller: 'conference', action: 'agenda')


        "/"(view:"/index")
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}

如果您现在使用 curl(或其他 HTTP 客户端)向其中一个 URL 发送非 GET 请求,您将收到一个 404 页面。使用以下 curl 命令来尝试

~ curl -X PUT localhost:8080/talks

5 组织 URL 映射

渐渐地,您的应用程序可能需要许多相似的 URL 映射,或者大量特定于应用程序某一部分的映射。有几种方法可以组织 URL 映射,包括 group 方法,以及使用多个 UrlMapping.groovy 文件。

URL 组

如果您有大量属于特定路径的映射,可以使用 group 方法来指定 URL 模式的共享部分,并且分别列出每种模式的右侧部分。

再次编辑 UrlMappings.groovy 文件,并在共享路径:/conf 下将添加的三种 URL 映射进行分组。

grails-app/controllers/demo/UrlMappings.groovy
package demo

class UrlMappings {

    static mappings = {
        //...

        group "/conf", {
            get "/talks/$id?"(controller: 'conference', action: 'talks')
            get "/speakers/$id?"(controller: 'conference', action: 'speakers')
            get "/agenda"(controller: 'conference', action: 'agenda')
        }

        //...
    }
}

如果您重启该应用程序,您将看到这些映射现在需要 /conf 作为 URL 模式的第一部分。例如,http://localhost:8080/conf/talks

多个 UrlMappings 文件

Grails 支持多个 UrlMappings 文件。唯一的要求是每个文件都必须具有唯一名称,存在于 grails-app/controllers 中,且文件名称必须以 UrlMappings.groovy 结尾。

我们为 ConferenceController 映射创建第二个 UrlMappings 文件。创建一个名为 ConferenceUrlMappings.groovy 的新文件,并按照如下内容编辑内容

grails-app/controllers/demo/ConferenceUrlMappings.groovy
package demo

class ConferenceUrlMappings {

    static mappings = {

        group "/conf", {
            get "/talks/$id?"(controller: 'conference', action: 'talks')
            get "/speakers/$id?"(controller: 'conference', action: 'speakers')
            get "/agenda"(controller: 'conference', action: 'agenda')
        }
    }
}

现在,从原始 UrlMappings.groovy 文件中删除 /conf URL 映射,使其与以下版本匹配

grails-app/controllers/demo/UrlMappings.groovy
package demo

class UrlMappings {

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

        "/"(view:"/index")
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}

6 测试 URL 映射

推荐使用 Grails 测试支持 库编写大多数 Grails 制品(包括 URL 映射)的测试。该库包含在当前版本的 Grails 中,并提供了一个 UrlMappingsUnitTest 性状,可以轻松测试 URL 映射。

性状是 Groovy 编程语言的一项强大功能,许多 Grails 功能都利用了它们。你也可以创建自己的性状,作为你的应用程序代码的一部分。请查阅 Groovy 文档 了解更多信息。

让我们为我们的 ConferenceUrlMappings 编写一个单元测试。在 src/test/groovy/demo/ 中创建一个新的 Groovy 文件,命名为 ConferenceUrlMappingsSpec.groovy。使用以下内容编辑该文件

src/test/groovy/demo/ConferenceUrlMappingsSpec.groovy
package demo

import grails.testing.web.UrlMappingsUnitTest
import spock.lang.Specification

class ConferenceUrlMappingsSpec extends Specification implements UrlMappingsUnitTest<ConferenceUrlMappings> { (1)

    void setup() {
        mockController(ConferenceController) (2)
    }

    void "test conference mappings"() {

        expect: "calls to /conference/talks to succeed"
        verifyUrlMapping("/conf/talks", controller: 'conference', action: 'talks', method: 'GET')  (3)
        verifyUrlMapping("/conf/talks/1", controller: 'conference', action: 'talks', method: 'GET') {
            id = '1'
        }

        when: "calling /conf/speakers"
        assertUrlMapping("/conf/speakers", controller: 'conference', action: 'speakers', method: 'GET') (4)

        then: "no exception is thrown"
        noExceptionThrown()

    }

}
1 我们的测试是标准 Spock 规范,然后实现 UrlMappingsUnitTest 性状(由测试支持库提供)。请注意,该性状接受泛型,即我们要测试的 URL 映射类;在这种情况下,为 ConferenceUrlMappings
2 测试库提供模拟助手,例如 mockController 来连接一个控制器实例以进行测试。
3 UrlMappingsUnitTest 性状提供多种测试方法,包括 verifyUrlMapping,如果 URL 映射与期望的控制器/动作匹配,则简单地返回真或假。
4 另一个测试方法是 assertUrlMapping。这会执行与 verifyUrlMapping 相同的检查,但允许你使用该方法作为声明的一部分(如 Spock when/then 块)。

7 结论

现在你应该熟悉 URL 映射的概念,并且你已经很好地掌握了最重要的一些功能。你可以从 Grails 文档 中了解有关 URL 映射功能的更多信息,包括名称空间、RESTful 资源映射、约束通配符

8 Grails 帮助

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

OCI 是 Grails 的家园

认识团队