import grails.rest.*
@Resource(uri='/books')
class Book {
String title
static constraints = {
title blank:false
}
}
9 REST
版本 6.2.0
目录
9 REST
REST 本身并不是一种技术,而更多是一种架构模式。REST 非常简单,其仅涉及使用纯 XML 或 JSON 作为通信媒介,再与底层系统的“表现性”URL 模式以及 GET、PUT、POST 和 DELETE 等 HTTP 方法相结合。
每个 HTTP 方法都映射到一个动作类型。例如,GET 用于检索数据,POST 用于创建数据,PUT 用于更新,依此类推。
Grails 包含让创建 RESTful API 变得简单的灵活特性。创建 RESTful 资源可以像下一部分演示的那样简单,只需一行代码。
9.1 域类作为 REST 资源
在 Grails 中创建 RESTful API 最简单的方法是将域类公开为 REST 资源。可通过向任何域类添加 grails.rest.Resource
转换来完成此操作
只需添加 Resource
转换并指定 URI,即可自动在 XML 或 JSON 格式中将您的域类用作 REST 资源。该转换将自动注册必要的 RESTful URL 映射,并创建一个名为 BookController
的控制器。
您可以通过向 BootStrap.groovy
添加一些测试数据来尝试
def init = { servletContext ->
new Book(title:"The Stand").save()
new Book(title:"The Shining").save()
}
然后访问 URL http://localhost:8080/books/1,它会呈现如下所示的响应
<?xml version="1.0" encoding="UTF-8"?>
<book id="1">
<title>The Stand</title>
</book>
如果您将 URL 更改为 http://localhost:8080/books/1.json,将获得如下 JSON 响应
{"id":1,"title":"The Stand"}
如果您希望更改默认设置,以便返回 JSON 而不是 XML,可通过设置 Resource
转换的 formats
属性来完成此操作
import grails.rest.*
@Resource(uri='/books', formats=['json', 'xml'])
class Book {
...
}
使用上述示例,JSON 将被优先考虑。传入的列表应包含资源应公开的格式的名称。格式名称在 application.groovy
的 grails.mime.types
设置中定义
grails.mime.types = [
...
json: ['application/json', 'text/json'],
...
xml: ['text/xml', 'application/xml']
]
请参阅用户指南中 配置 MIME 类型 一节,了解更多信息。
除了使用 URI 中的文件扩展名之外,您还可以使用 ACCEPT 头来获取 JSON 响应。以下是使用 Unix curl
工具的一个示例
$ curl -i -H "Accept: application/json" localhost:8080/books/1
{"id":1,"title":"The Stand"}
这要归功于 Grails 的 内容协商 特性。
您可以通过发出 POST
请求创建新的资源
$ curl -i -X POST -H "Content-Type: application/json" -d '{"title":"Along Came A Spider"}' localhost:8080/books
HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
...
可使用 PUT
请求进行更新
$ curl -i -X PUT -H "Content-Type: application/json" -d '{"title":"Along Came A Spider"}' localhost:8080/books/1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
...
最后,可以使用 DELETE
请求删除资源
$ curl -i -X DELETE localhost:8080/books/1
HTTP/1.1 204 No Content
Server: Apache-Coyote/1.1
...
正如您所看到的,Resource
转换启用了资源上的所有 HTTP 方法动词。您可以通过将 readOnly
属性设置为 true 仅启用只读功能
import grails.rest.*
@Resource(uri='/books', readOnly=true)
class Book {
...
}
在这种情况下,将禁止 POST
、PUT
和 DELETE
请求。
9.2 映射到 REST 资源
如果您更喜欢将 URL 映射的声明保留在 UrlMappings.groovy
文件中,那么只需删除 Resource
转换的 uri
属性,并在 UrlMappings.groovy
中添加以下行就足够了
"/books"(resources:"book")
然后扩展 API 以包含更多终结点变得简单
"/books"(resources:"book") {
"/publisher"(controller:"publisher", method:"GET")
}
上述示例将公开 URI /books/1/publisher
。
可在用户指南的URL 映射部分中找到有关创建 RESTful URL 映射的更详细说明。
9.3 从 GSP 页面链接到 REST 资源
link
标签提供了一种轻松链接到任意领域类资源的方式
<g:link resource="${book}">My Link</g:link>
但是,当前无法使用 g:link 链接到 DELETE 操作,并且大多数浏览器不支持直接发送 DELETE 方法。
实现此操作的最佳方式是使用表单提交
<form action="/book/2" method="post">
<input type="hidden" name="_method" value="DELETE"/>
</form>
Grails 支持通过隐藏的 _method
参数覆盖请求方法。此操作出于浏览器兼容性目的。在使用 restful 资源映射创建功能强大的 Web 接口时,此操作非常有用。要使链接触发此类型事件,建议捕获所有具有 data-method
属性的链接的点击事件,并通过 JavaScript 发出表单提交。
9.4 对 REST 资源进行版本控制
REST API 的常见要求是同时公开不同的版本。可在 Grails 中通过一些方法实现此功能。
使用 URI 进行版本控制
一种常见方法是使用 URI 对 API 进行版本控制(虽然不推荐使用此方法,而是推荐使用超媒体)。例如,可以定义以下 URL 映射
"/books/v1"(resources:"book", namespace:'v1')
"/books/v2"(resources:"book", namespace:'v2')
这将匹配以下控制器
package myapp.v1
class BookController {
static namespace = 'v1'
}
package myapp.v2
class BookController {
static namespace = 'v2'
}
此方法的缺点在于要求 API 有两个不同的 URI 命名空间。
使用 Accept-Version 头进行版本控制
作为替代方案,Grails 支持从客户端传递 Accept-Version
头。例如,可以定义以下 URL 映射
"/books"(version:'1.0', resources:"book", namespace:'v1')
"/books"(version:'2.0', resources:"book", namespace:'v2')
然后,在客户端中,只需使用 Accept-Version
头传递所需的版本
$ curl -i -H "Accept-Version: 1.0" -X GET http://localhost:8080/books
使用超媒体/Mime 类型进行版本控制
进行版本控制的另一种方法是使用 Mime 类型定义来声明自定义媒体类型的版本(有关超媒体概念的更多信息,请参阅“用作应用程序状态引擎的超媒体”一節)。例如,可在 application.groovy
中为资源声明一个自定义 Mime 类型,其中包括版本参数('v' 参数)
grails.mime.types = [
all: '*/*',
book: "application/vnd.books.org.book+json;v=1.0",
bookv2: "application/vnd.books.org.book+json;v=2.0",
...
}
关键是在 'all' Mime 类型后放置新的 Mime 类型,因为如果无法建立请求的内容类型,则会将映射中的第一个条目用于响应。如果在顶部放置新的 Mime 类型,则在无法建立请求的 Mime 类型时,Grails 将始终尝试发送回新的 Mime 类型。 |
然后,覆盖呈现器(有关自定义呈现器的更多信息,请参阅“自定义响应呈现”一節),以便在 grails-app/conf/spring/resourses.groovy
中发回自定义 Mime 类型
import grails.rest.render.json.*
import grails.web.mime.*
beans = {
bookRendererV1(JsonRenderer, myapp.v1.Book, new MimeType("application/vnd.books.org.book+json", [v:"1.0"]))
bookRendererV2(JsonRenderer, myapp.v2.Book, new MimeType("application/vnd.books.org.book+json", [v:"2.0"]))
}
然后,更新控制器中可接受的响应格式列表
class BookController extends RestfulController {
static responseFormats = ['json', 'xml', 'book', 'bookv2']
// ...
}
然后,可以使用 Accept
头,并使用 Mime 类型指定所需的版本
$ curl -i -H "Accept: application/vnd.books.org.book+json;v=1.0" -X GET http://localhost:8080/books
9.5 实现 REST 控制器
Resource
转换是一种快速入门的方法,但通常您会需要自定义控制器逻辑、响应渲染或扩展 API 以包括附加动作。
9.5.1 扩展 RestfulController 超类
开始执行此操作的最简单方法是为资源创建新控制器,该资源扩展 grails.rest.RestfulController
超类。例如
class BookController extends RestfulController<Book> {
static responseFormats = ['json', 'xml']
BookController() {
super(Book)
}
}
若要自定义任何逻辑,只需替换相应的动作即可。下表提供了动作名称及其映射到的 URI
HTTP 方法 | URI | 控制器动作 |
---|---|---|
GET |
/books |
index |
GET |
/books/create |
create |
POST |
/books |
save |
GET |
/books/${id} |
show |
GET |
/books/${id}/edit |
edit |
PUT |
/books/${id} |
update |
DELETE |
/books/${id} |
delete |
如果控制器公开了 HTML 界面,则只需要 create 和 edit 动作。 |
例如,如果您有一个嵌套资源,通常您会想要查询父标识符和子标识符。例如,给定以下 URL 映射
"/authors"(resources:'author') {
"/books"(resources:'book')
}
您可以按如下方法实现嵌套控制器
class BookController extends RestfulController {
static responseFormats = ['json', 'xml']
BookController() {
super(Book)
}
@Override
protected Book queryForResource(Serializable id) {
Book.where {
id == id && author.id == params.authorId
}.find()
}
}
上例的子类是 RestfulController
,并替换受保护的 queryForResource
方法以自定义查询,查询资源会考虑父资源。
在 RestfulController 子类中自定义数据绑定
RestfulController 类包含执行数据绑定的代码,用于执行诸如 save
和 update
之类动作。该类定义一个 getObjectToBind()
方法,该方法返回一个值,该值将用作数据绑定的源。例如,更新动作执行类似操作…
class RestfulController<T> {
def update() {
T instance = // retrieve instance from the database...
instance.properties = getObjectToBind()
// ...
}
// ...
}
默认情况下,getObjectToBind()
方法返回request 对象。当 request
对象用作绑定源时,如果请求包含正文,则将分析正文并使用其内容执行数据绑定,否则将使用请求参数执行数据绑定。RestfulController 的子类可能会替换 getObjectToBind()
方法,并返回任何有效的绑定源,包括Map 或DataBindingSource。对于大多数用例,绑定请求是合适的,但 getObjectToBind()
方法允许在需要时更改该行为。
使用带 Resource 批注的 RestfulController 自定义子类
您还可以自定义支持 Resource 批注的控制器的行为。
该类必须提供一个构造函数,该函数将域类作为其参数。需要第二个构造函数来支持带有 readOnly=true 的 Resource 批注。
这是可用于 Resource 批注中使用的 RestfulController 子类化类的模板
class SubclassRestfulController<T> extends RestfulController<T> {
SubclassRestfulController(Class<T> domainClass) {
this(domainClass, false)
}
SubclassRestfulController(Class<T> domainClass, boolean readOnly) {
super(domainClass, readOnly)
}
}
您可以通过 superClass
属性指定支持 Resource 注解的控制器的超类。
import grails.rest.*
@Resource(uri='/books', superClass=SubclassRestfulController)
class Book {
String title
static constraints = {
title blank:false
}
}
9.5.2 分步实现 REST 控制器
如果您不想利用 RestfulController
超类提供的功能,则可以手动实现每个 HTTP 动词。第一步是创建一个控制器
$ grails create-controller book
然后添加一些有用的导入并默认启用 readOnly
import grails.gorm.transactions.*
import static org.springframework.http.HttpStatus.*
import static org.springframework.http.HttpMethod.*
@Transactional(readOnly = true)
class BookController {
...
}
回想一下,按照以下惯例,每个 HTTP 动词都与一个特定的 Grails 动作匹配
HTTP 方法 | URI | 控制器动作 |
---|---|---|
GET |
/books |
index |
GET |
/books/${id} |
show |
GET |
/books/create |
create |
GET |
/books/${id}/edit |
edit |
POST |
/books |
save |
PUT |
/books/${id} |
update |
DELETE |
/books/${id} |
delete |
如果您计划为 REST 资源实现 HTML 界面,则 create 和 edit 动作已经是必需的。它们用于呈现创建和编辑资源的适当 HTML 表单。如果不满足此要求,可以不考虑它们。 |
实现 REST 动作的关键是 respond 方法,此方法在 Grails 2.3 中引入。respond
方法会尝试为请求的内容类型(JSON、XML、HTML 等)生成最合适的响应
实现 'index' 动作
例如,要实现 index
动作,只需调用 respond
方法,并将要响应的对象列表传入
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
respond Book.list(params), model:[bookCount: Book.count()]
}
请注意,在上述示例中,我们还使用了 respond
方法的 model
参数来提供总数。仅当您计划通过某个用户界面支持分页时才需要这样做。
respond
方法会使用 内容协商 根据客户端请求的内容类型(通过 ACCEPT 头或文件扩展名)尝试回复最合适的响应。
如果将内容类型建立为 HTML,系统会生成一个模型,以便上述动作相当于编写
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
[bookList: Book.list(params), bookCount: Book.count()]
}
通过提供 index.gsp
文件,您可以为给定的模型呈现适当的视图。如果内容类型不是 HTML,则 respond
方法会尝试查找能够呈现已通过对象的适当 grails.rest.render.Renderer
实例。此操作通过检查 grails.rest.render.RendererRegistry
来实现。
默认情况下,已经为 JSON 和 XML 配置了呈现器,要了解如何注册自定义呈现器,请参阅“自定义响应呈现”一节。
实现 'show' 动作
show
动作用于按 id 显示单个资源,它可以用一行 Groovy 代码实现(不包括方法签名)
def show(Book book) {
respond book
}
通过将域实例指定为对动作的参数,Grails 将自动尝试使用请求的 id
参数来查找域实例。如果域实例不存在,那么 null
将传递到动作中。如果 respond
方法否决 null,则它将返回一个 404 错误,否则它将再次尝试呈现一个适当的响应。如果格式是 HTML,那么将制作一个适当的模型。以下动作在功能上等同于上述动作
def show(Book book) {
if(book == null) {
render status:404
}
else {
return [book: book]
}
}
实现“保存”动作
save
动作创建新的资源表示。为了开始,只需定义一个动作,将资源作为第一个参数接受,并使用 grails.gorm.transactions.Transactional
转换将其标记为 Transactional
@Transactional
def save(Book book) {
...
}
然后首先要做的是检查资源是否具有任何验证错误 如果有,则通过错误进行响应
if(book.hasErrors()) {
respond book.errors, view:'create'
}
else {
...
}
对于 HTML,“create”视图将再次呈现,以便用户可以纠正无效的输入。对于其他格式(JSON、XML 等),错误对象本身将以适当的格式呈现,并返回一个状态代码 422(UNPROCESSABLE_ENTITY)。
如果没有错误,那么可以保存资源并发送一个适当的响应
book.save flush:true
withFormat {
html {
flash.message = message(code: 'default.created.message', args: [message(code: 'book.label', default: 'Book'), book.id])
redirect book
}
'*' { render status: CREATED }
}
对于 HTML,将发布一个重定向到原始资源,对于其他格式,将返回一个状态代码 201(CREATED)。
实现“更新”动作
update
动作更新现有的资源表示,并且在很大程度上类似于 save
动作。首先定义方法签名
@Transactional
def update(Book book) {
...
}
如果资源存在,那么 Grails 将加载资源,否则会传递 null。对于 null,你应该返回一个 404
if(book == null) {
render status: NOT_FOUND
}
else {
...
}
然后再次检查是否有错误验证错误,如果有,则通过错误进行响应
if(book.hasErrors()) {
respond book.errors, view:'edit'
}
else {
...
}
对于 HTML,“edit”视图将再次呈现,以便用户可以纠正无效的输入。对于其他格式(JSON、XML 等),错误对象本身将以适当的格式呈现,并返回一个状态代码 422(UNPROCESSABLE_ENTITY)。
如果没有错误,那么可以保存资源并发送一个适当的响应
book.save flush:true
withFormat {
html {
flash.message = message(code: 'default.updated.message', args: [message(code: 'book.label', default: 'Book'), book.id])
redirect book
}
'*' { render status: OK }
}
对于 HTML,将发布一个重定向到原始资源,对于其他格式,将返回一个状态代码 200(OK)。
实现“删除”动作
delete
动作删除现有的资源。除了调用 delete()
方法之外,其实现与 update
动作非常相似
book.delete flush:true
withFormat {
html {
flash.message = message(code: 'default.deleted.message', args: [message(code: 'Book.label', default: 'Book'), book.id])
redirect action:"index", method:"GET"
}
'*'{ render status: NO_CONTENT }
}
请注意,对于 HTML 响应,将发布一个重定向返回到 index
动作,而对于其他内容类型,将返回响应代码 204(NO_CONTENT)。
9.5.3 使用脚手架生成一个 REST 控制器
为了在实际操作中看到这些概念并帮助你入门,Scaffolding 插件(2.0 及更高版本)可以为你生成一个 REST 就绪控制器,只需运行该命令
$ grails generate-controller <<Domain Class Name>>
9.6 使用 HttpClient 调用 REST 服务
使用 Micronaut HTTP 客户端 调用 Grails REST 服务(以及第三方服务)非常简单。此 HTTP 客户端同时具有底层 API 和更高级别的 AOP 驱动的 API,使其可用于简单请求和构建声明性、类型安全的 API 层。
要使用 Micronaut HTTP 客户端,你的类路径中必须有 micronaut-http-client
依赖项。将以下依赖项添加到你的 build.gradle
文件中。
implementation 'io.micronaut:micronaut-http-client'
底层 API
HttpClient 接口构成了底层 API 的基础。此接口声明了方法,以帮助简化执行 HTTP 请求并接收响应。
HttpClient
接口中的大多数方法返回 Reactive Streams Publisher 实例,并且包含了一个称为 RxHttpClient 的子接口,它提供了返回 RxJava Flowable 类型的一个 HttpClient 接口变体。在阻塞流中使用 HttpClient
时,你可能希望调用 toBlocking()
以返回一个 BlockingHttpClient 实例。
有几种方法可以获取对 HttpClient 的引用。最简单的方法是使用 create 方法
List<Album> searchWithApi(String searchTerm) {
String baseUrl = "https://itunes.apple.com/"
HttpClient client = HttpClient.create(baseUrl.toURL()).toBlocking() (1)
HttpRequest request = HttpRequest.GET("/search?limit=25&media=music&entity=album&term=${searchTerm}")
HttpResponse<String> resp = client.exchange(request, String)
client.close() (2)
String json = resp.body()
ObjectMapper objectMapper = new ObjectMapper() (3)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
SearchResult searchResult = objectMapper.readValue(json, SearchResult)
searchResult.results
}
1 | 使用 create 方法创建一个新的 HttpClient 实例,并用 toBlocking() 转换为 BlockingHttpClient 的实例, |
2 | 应使用 close 方法关闭客户端以防止线程泄漏。 |
3 | Jackson 的 ObjectMapper API 可用于将原始 JSON 映射到 POGO,在本例中为 SearchResult |
有关使用 HttpClient
底层 API 的详细信息,请参阅 Http 客户端部分 的 Micronaut 用户指南。
声明性 API
可以通过对任意接口或抽象类添加 @Client
批注来编写声明性 HTTP 客户端。使用 Micronaut 的 AOP 支持(参见 引言建议 的 Micronaut 用户指南部分),抽象或接口方法将在编译时作为 HTTP 调用为你实现。声明性客户端可以返回数据绑定的 POGO(或 POJO),而无需调用代码进行特殊处理。
package example.grails
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@Client("https://start.grails.org")
interface GrailsAppForgeClient {
@Get("/{version}/profiles")
List<Map> profiles(String version)
}
请注意,HTTP 客户端方法用适当的 HTTP 方法(例如 @Get
或 @Post
)进行了批注。
要使用类似于上述示例中的客户端,只需使用 @Autowired
注解将客户端实例注入到任何 bean 中。
@Autowired GrailsAppForgeClient appForgeClient
List<Map> profiles(String grailsVersion) {
respond appForgeClient.profiles(grailsVersion)
}
有关编写和使用声明式客户端的详细信息,请查阅 Micronaut 用户指南 中的 HTTP 客户端部分。
9.7 REST 配置文件
自 Grails 3.1 以来,Grails 支持为创建 REST 应用程序量身定制的配置文件,该配置文件提供了一组更集中的依赖项和命令。
要开始使用 REST API 类型应用程序,请执行以下操作
$ grails create-restapi my-api
这会创建一个新的 REST 应用程序,该应用程序提供以下功能
-
用于创建和生成 REST 端点的默认命令集
-
默认为使用 JSON 视图进行呈现响应(请参阅下一部分)
-
较之默认的 Grails Web 风格应用程序,插件较少(没有 GSP,没有 Asset Pipeline,没有任何与 HTML 相关的)
例如,您将注意到在 grails-app/views
目录中,有一些 *.gson
文件,用于呈现默认索引页面以及任何 404 和 500 错误。
如果您发出以下命令集
$ grails create-domain-class my.api.Book
$ ./gradlew runCommand -Pargs="generate-all my.api.Book"
generate-* 命令仅在将 org.grails.plugins:scaffolding 依赖项添加到项目后才可用。它们在 REST 应用程序中默认不可用。此外,它们将不再生成 *。gson 文件,因为这是 REST API 配置文件的功能。个人资料已在 Grails 6 中移除。 |
生成了替代 CRUD HTML 界面来生成 JSON 响应的 REST 端点。此外,默认情况下,生成的函数和单元测试会测试 REST 端点。
9.8 JSON 视图
正如上一部分所述,REST 配置文件默认使用 JSON 视图来呈现 JSON 响应。这些视图与 GSP 相似,但经过优化,可以输出 JSON 响应而不是 HTML。
您可以继续按照 MVC 划分应用程序,应用程序的逻辑驻留在控制器和服务中,而视图相关事项由 JSON 视图处理。
JSON 视图还提供了轻松自定义呈现给客户端的 JSON 的灵活性,而无需诉诸相对复杂的编组库(如 Jackson 或 Grails 的编组 API)。
自 Grails 3.1 以来,JSON 视图被 Grails 团队视为向客户端呈现 JSON 输出的最佳方式,有关编写自定义编组器的章节已从用户指南中移除。如果您正在寻找有关该主题的信息,请参阅 Grails 3.0.x 指南。 |
9.8.1 开始
如果您使用的是 REST 应用程序,那么 JSON 视图插件将已经包含在内,您可以跳过本节的其余部分。否则,您需要修改 build.gradle
以包括必要的插件以激活 JSON 视图
implementation 'org.grails.plugins:views-json:1.0.0' // or whatever is the latest version
如果您正在寻找更多文档和贡献,可以在 Github 上找到 JSON 视图的 源代码存储库 |
要编译 JSON 视图以用于生产部署,您还应首先修改 buildscript
代码块以启用 Gradle 插件
buildscript {
...
dependencies {
...
classpath "org.grails.plugins:views-gradle:1.0.0"
}
}
然后在所有 Grails 核心 Gradle 插件之后应用 org.grails.plugins.views-json
Gradle 插件
...
apply plugin: "org.grails.grails-web"
apply plugin: "org.grails.plugins.views-json"
这会向 Gradle 添加一个 compileGsonViews
任务,该任务会在创建生产 JAR 或 WAR 文件之前调用。
9.8.2 创建 JSON 视图
JSON 视图位于 grails-app/views
目录中,并以 .gson
后缀结尾。它们是常规 Groovy 脚本,可以在任何 Groovy 编辑器中打开。
JSON 视图示例
json.person {
name "bob"
}
要在 IntelliJ IDEA 中的 Groovy 编辑器中打开它们,请双击该文件,并在询问要将其关联到哪个文件时,选择“Groovy” |
上述 JSON 视图生成
{"person":{"name":"bob"}}
有一个隐式的 json
变量,它是一个 StreamingJsonBuilder 实例。
用法示例
json(1,2,3) == "[1,2,3]"
json { name "Bob" } == '{"name":"Bob"}'
json([1,2,3]) { n it } == '[{"n":1},{"n":2},{"n":3}]'
请参阅 StreamingJsonBuilder 上的 API 文档,以获取有关可能性的更多信息。
9.8.3 JSON 视图模板
您可以定义以下划线 _
开头的模板。例如,给定名为 _person.gson
的以下模板
model {
Person person
}
json {
name person.name
age person.age
}
您可以使用视图按如下方式呈现它
model {
Family family
}
json {
name family.father.name
age family.father.age
oldestChild g.render(template:"person", model:[person: family.children.max { Person p -> p.age } ])
children g.render(template:"person", collection: family.children, var:'person')
}
或者,使用 tmpl 变量以更简洁的方式调用模板
model {
Family family
}
json {
name family.father.name
age family.father.age
oldestChild tmpl.person( family.children.max { Person p -> p.age } ] )
children tmpl.person( family.children )
}
9.8.4 使用 JSON 视图呈现域类
通常,您的模型可能涉及一个或多个域实例。JSON 视图为呈现这些实例提供了一个呈现方法。
例如,给定以下域类
class Book {
String title
}
以及以下模板
model {
Book book
}
json g.render(book)
生成的结果是
{id:1, title:"The Stand"}
您可以通过包含或排除属性来自定义呈现
json g.render(book, [includes:['title']])
或者通过提供一个闭包来添加额外的 JSON 输出
json g.render(book) {
pages 1000
}
9.8.5 根据约定使用 JSON 视图
在创建 JSON 视图时,您可以遵循一些有用的约定。例如,如果您有一个名为 Book
的域类,那么在 grails-app/views/book/_book.gson
位置创建一个模板,并使用 respond 方法将呈现模板
def show(Long id) {
respond Book.get(id)
}
此外,如果在验证过程中发生错误,Grails 默认会尝试呈现一个名为 grails-app/views/book/_errors.gson
的模板,否则,如果前者不存在,它将尝试呈现 grails-app/views/errors/_errors.gson
。
这很有用,因为当持久化对象时,您可以用验证错误 respond
来呈现这些前述模板
@Transactional
def save(Book book) {
if (book.hasErrors()) {
transactionStatus.setRollbackOnly()
respond book.errors
}
else {
// valid object
}
}
如果在上述示例中发生验证错误,将呈现 grails-app/views/book/_errors.gson
模板。
有关 JSON 视图(和标记视图)的更多信息,请参阅 JSON 视图用户指南。
9.9 自定义响应呈现
如果您正在寻找更低级别的 API 并且 JSON 或标记视图不适合您的需求,那么您可能要考虑实现一个自定义呈现器。
9.9.1 自定义默认呈现器
XML 和 JSON 默认的渲染器分别可以在 grails.rest.render.xml
和 grails.rest.render.json
包中找到。默认情况下,这些渲染器使用 Grails 转换器(grails.converters.XML
和 grails.converters.JSON
)进行响应呈现。
你可以轻松地使用这些默认渲染器自定义响应呈现。你可能想要做的一个常见的改动就是包含或排除某些属性不呈现。
包含或排除属性不呈现
如前所述,Grails 维护了 grails.rest.render.Renderer
实例的注册表。有一些默认配置的渲染器,并且有为给定的域类甚至域类集合注册或覆盖渲染器。若要包含特定属性不呈现,你需要通过在 grails-app/conf/spring/resources.groovy
中定义 bean 来注册自定义渲染器。
import grails.rest.render.xml.*
beans = {
bookRenderer(XmlRenderer, Book) {
includes = ['title']
}
}
bean 名称并不重要(Grails 将扫描应用程序上下文以查找所有已注册的渲染器 bean),但出于组织和可读性目的,建议你为其命名一些有意义的内容。 |
若要排除属性,可以使用 XmlRenderer
类的 excludes
属性。
import grails.rest.render.xml.*
beans = {
bookRenderer(XmlRenderer, Book) {
excludes = ['isbn']
}
}
自定义转换器
如前所述,默认呈现使用底层的 grails.converters
包。换句话说,它们在底层本质上执行以下操作:
import grails.converters.*
...
render book as XML
// or render book as JSON
9.9.2 实现自定义渲染器
如果你想对呈现有更多控制或更喜欢使用自己的编组技术,那么可以实现你自己的 Renderer
实例。例如,下面是一个简单的实现,用于自定义 Book
类的呈现:
package myapp
import grails.rest.render.*
import grails.web.mime.MimeType
class BookXmlRenderer extends AbstractRenderer<Book> {
BookXmlRenderer() {
super(Book, [MimeType.XML,MimeType.TEXT_XML] as MimeType[])
}
void render(Book object, RenderContext context) {
context.contentType = MimeType.XML.name
def xml = new groovy.xml.MarkupBuilder(context.writer)
xml.book(id: object.id, title:object.title)
}
}
AbstractRenderer
超类有一个构造函数,它采用它呈现的类和渲染器接受的 MimeType
(通过 ACCEPT 头或文件扩展名)。
要配置此渲染器,只需将它添加到 grails-app/conf/spring/resources.groovy
中即可。
beans = {
bookRenderer(myapp.BookXmlRenderer)
}
结果将是所有 Book
实例都将以以下格式呈现:
<book id="1" title="The Stand"/>
如果你将呈现更改为一个完全不同的格式(如上所示),那么如果你计划支持 POST 和 PUT 请求,那么你还需要更改绑定。否则,Grails 将不会自动知道如何将数据从自定义 XML 格式绑定到域类。有关更多信息,请参见"自定义资源绑定"一节。 |
容器渲染器
grails.rest.render.ContainerRenderer
是一种为对象容器(列表、地图、集合等)呈现响应的渲染器。界面与 Renderer
界面在很大程度上相同,只是增加了 getComponentType()
方法,该方法应返回“包含”的类型。例如
class BookListRenderer implements ContainerRenderer<List, Book> {
Class<List> getTargetType() { List }
Class<Book> getComponentType() { Book }
MimeType[] getMimeTypes() { [ MimeType.XML] as MimeType[] }
void render(List object, RenderContext context) {
....
}
}
9.9.3 使用 GSP 来自定义呈现
还可以使用 Groovy 服务器页面 (GSP) 以按操作自定义呈现。例如,给定前面提到的 show
操作
def show(Book book) {
respond book
}
可以提供 show.xml.gsp
文件来自定义 XML 的呈现
<%@page contentType="application/xml"%>
<book id="${book.id}" title="${book.title}"/>
9.10 超文本媒体作为应用程序状态引擎
HATEOAS(超文本媒体作为应用程序状态引擎的缩写)是一种常见的模式,用于通过超文本媒体和链接来定义 REST API 的 REST 架构。
超文本媒体(也称为 MIME 或媒体类型)用于描述 REST 资源的状态,而链接告诉客户端如何过渡到下一个状态。响应的格式通常为 JSON 或 XML,尽管经常使用 Atom 和/或 HAL 等标准格式。
9.10.1 HAL 支持
HAL 是一种标准交换格式,通常用于开发遵循 HATEOAS 原则的 REST API。下面是表示订单列表的 HAL 文档示例
{
"_links": {
"self": { "href": "/orders" },
"next": { "href": "/orders?page=2" },
"find": {
"href": "/orders{?id}",
"templated": true
},
"admin": [{
"href": "/admins/2",
"title": "Fred"
}, {
"href": "/admins/5",
"title": "Kate"
}]
},
"currentlyProcessing": 14,
"shippedToday": 20,
"_embedded": {
"order": [{
"_links": {
"self": { "href": "/orders/123" },
"basket": { "href": "/baskets/98712" },
"customer": { "href": "/customers/7809" }
},
"total": 30.00,
"currency": "USD",
"status": "shipped"
}, {
"_links": {
"self": { "href": "/orders/124" },
"basket": { "href": "/baskets/97213" },
"customer": { "href": "/customers/12369" }
},
"total": 20.00,
"currency": "USD",
"status": "processing"
}]
}
}
使用 HAL 公开资源
要为资源返回 HAL 而不是常规 JSON,你可以简单地用 grails.rest.render.hal.HalJsonRenderer
(或 HalXmlRenderer
,用于 XML 变体)的实例覆盖 grails-app/conf/spring/resources.groovy
中的渲染器
import grails.rest.render.hal.*
beans = {
halBookRenderer(HalJsonRenderer, rest.test.Book)
}
你还需要更新资源的可接受响应格式,以包括 HAL 格式。不执行此操作将导致服务器返回 406 - 不接受响应。
这可以通过设置 Resource
转换的 formats
属性来完成
import grails.rest.*
@Resource(uri='/books', formats=['json', 'xml', 'hal'])
class Book {
...
}
或通过更新控制器中的 responseFormats
class BookController extends RestfulController {
static responseFormats = ['json', 'xml', 'hal']
// ...
}
在 bean 存在的情况下,请求 HAL 内容类型将返回 HAL
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books/1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/hal+json;charset=ISO-8859-1
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "\"The Stand\""
}
要使用 HAL XML 格式,只需更改渲染器
import grails.rest.render.hal.*
beans = {
halBookRenderer(HalXmlRenderer, rest.test.Book)
}
使用 HAL 呈现集合
要为资源列表返回 HAL 而不是常规 JSON,你可以简单地用 grails.rest.render.hal.HalJsonCollectionRenderer
的实例覆盖 grails-app/conf/spring/resources.groovy
中的渲染器
import grails.rest.render.hal.*
beans = {
halBookCollectionRenderer(HalJsonCollectionRenderer, rest.test.Book)
}
在 bean 存在的情况下,请求 HAL 内容类型将返回 HAL
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/hal+json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 17 Oct 2013 02:34:14 GMT
{
"_links": {
"self": {
"href": "http://localhost:8080/books",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"book": [
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "The Stand"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Infinite Jest"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/3",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Walden"
}
]
}
}
请注意,呈现的 JSON 中与 Book
对象列表关联的键是 book
,它派生自集合中的对象类型,即 Book
。要自定义此键的值,请在 HalJsonCollectionRenderer
bean 上为 collectionName
属性分配一个值,如下所示
import grails.rest.render.hal.*
beans = {
halBookCollectionRenderer(HalCollectionJsonRenderer, rest.test.Book) {
collectionName = 'publications'
}
}
有了它,呈现的 HAL 将如下所示
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/hal+json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 17 Oct 2013 02:34:14 GMT
{
"_links": {
"self": {
"href": "http://localhost:8080/books",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"publications": [
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "The Stand"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Infinite Jest"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/3",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Walden"
}
]
}
}
使用自定义媒体/Mime 类型
如果您希望使用自定义 Mime 类型,则首先需要在 grails-app/conf/application.groovy
中声明 Mime 类型
grails.mime.types = [
all: "*/*",
book: "application/vnd.books.org.book+json",
bookList: "application/vnd.books.org.booklist+json",
...
]
关键是在 'all' Mime 类型后放置新的 Mime 类型,因为如果无法建立请求的内容类型,则会将映射中的第一个条目用于响应。如果在顶部放置新的 Mime 类型,则在无法建立请求的 Mime 类型时,Grails 将始终尝试发送回新的 Mime 类型。 |
然后重写渲染器以使用自定义 Mime 类型返回 HAL
import grails.rest.render.hal.*
import grails.web.mime.*
beans = {
halBookRenderer(HalJsonRenderer, rest.test.Book, new MimeType("application/vnd.books.org.book+json", [v:"1.0"]))
halBookListRenderer(HalJsonCollectionRenderer, rest.test.Book, new MimeType("application/vnd.books.org.booklist+json", [v:"1.0"]))
}
在上方的示例中,第一个 bean 为返回 Mime 类型为 application/vnd.books.org.book+json
的单个图书实例定义了一个 HAL 渲染器。第二个 bean 定义了用于渲染图书集合的 Mime 类型(在本例中为 application/vnd.books.org.booklist+json
)。
application/vnd.books.org.booklist+json 是媒体范围的一个示例 (http://www.w3.org/Protocols/rfc2616/rfc2616.html - 标题字段定义)。该示例使用实体 (图书) 和操作 (列表) 形成媒体范围值,但实际上不必为每个操作创建单独的 Mime 类型。此外,不必在实体级别创建 Mime 类型。有关如何定义您自己的 Mime 类型的详细信息,请参阅“版本控制 REST 资源”部分。 |
有了这个,对新 Mime 类型发出请求将会返回必要的 HAL
$ curl -i -H "Accept: application/vnd.books.org.book+json" http://localhost:8080/books/1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/vnd.books.org.book+json;charset=ISO-8859-1
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/vnd.books.org.book+json"
}
},
"title": "\"The Stand\""
}
自定义链接渲染
HATEOAS 的一个重要方面是使用描述客户端可用于与 REST API 交互的转换的链接。默认情况下,HalJsonRenderer
会自动为您创建关联和资源本身的链接(使用“self”关系)。
但是,您可以使用已添加到所有使用 grails.rest.Resource
注释的域类或使用 grails.rest.Linkable
注释的任何类中的 link
方法来自定义链接渲染。例如,可以按如下方式修改 show
操作以在结果输出中提供一个新链接
def show(Book book) {
book.link rel:'publisher', href: g.createLink(absolute: true, resource:"publisher", params:[bookId: book.id])
respond book
}
这将生成诸如以下内容的输出
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/vnd.books.org.book+json"
}
"publisher": {
"href": "http://localhost:8080/books/1/publisher",
"hreflang": "en"
}
},
"title": "\"The Stand\""
}
link
方法可以传入与 grails.rest.Link
类属性匹配的命名参数。
9.10.2 Atom 支持
Atom 是另一种用于实现 REST API 的标准交换格式。下面是可以看到的 Atom 输出的一个示例
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<link href="http://example.org/"/>
<updated>2003-12-13T18:30:02Z</updated>
<author>
<name>John Doe</name>
</author>
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
<entry>
<title>Atom-Powered Robots Run Amok</title>
<link href="http://example.org/2003/12/13/atom03"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
</entry>
</feed>
要使用 Atom 渲染,只需再次定义一个自定义渲染器
import grails.rest.render.atom.*
beans = {
halBookRenderer(AtomRenderer, rest.test.Book)
halBookListRenderer(AtomCollectionRenderer, rest.test.Book)
}
9.10.3 Vnd.Error 支持
Vnd.Error 是一种表达错误响应的标准化方法。
默认情况下,当尝试 POST 新资源时,如果发生验证错误,则会返回 errors 对象,并允许使用 422 响应代码
$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d "" http://localhost:8080/books
HTTP/1.1 422 Unprocessable Entity
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=ISO-8859-1
{
"errors": [
{
"object": "rest.test.Book",
"field": "title",
"rejected-value": null,
"message": "Property [title] of class [class rest.test.Book] cannot be null"
}
]
}
如果您希望将格式更改为 Vnd.Error,只需在 grails-app/conf/spring/resources.groovy
中注册 grails.rest.render.errors.VndErrorJsonRenderer
bean
beans = {
vndJsonErrorRenderer(grails.rest.render.errors.VndErrorJsonRenderer)
// for Vnd.Error XML format
vndXmlErrorRenderer(grails.rest.render.errors.VndErrorXmlRenderer)
}
然后,如果您更改客户端请求以接受 Vnd.Error,您将得到一个适当的响应
$ curl -i -H "Accept: application/vnd.error+json,application/json" -H "Content-Type: application/json" -X POST -d "" http://localhost:8080/books
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/vnd.error+json;charset=ISO-8859-1
[
{
"logref": "book.nullable,
"message": "Property [title] of class [class rest.test.Book] cannot be null",
"_links": {
"resource": {
"href": "http://localhost:8080/rest-test/books"
}
}
}
]
9.11 自定义资源的绑定
该框架提供了一种复杂但简单的机制,用于将 REST 请求绑定到域对象和命令对象。可以使用这种机制的一种方法是将控制器中的 request
属性绑定到域类的 properties
。假设以下 XML 作为请求的正文,则 createBook
动作将创建一个新的 Book
,并将“The Stand”分配给 title
属性,并将“Stephen King”分配给 authorName
属性。
<?xml version="1.0" encoding="UTF-8"?>
<book>
<title>The Stand</title>
<authorName>Stephen King</authorName>
</book>
class BookController {
def createBook() {
def book = new Book()
book.properties = request
// ...
}
}
命令对象将自动绑定到请求正文
class BookController {
def createBook(BookCommand book) {
// ...
}
}
class BookCommand {
String title
String authorName
}
如果命令对象类型是域类,并且 XML 文档的根元素包含一个 id
属性,则会使用 id
值从数据库中检索相应的持久实例,然后将文档的其余部分绑定到该实例上。如果在数据库中找不到相应的记录,则命令对象引用将为 null。
<?xml version="1.0" encoding="UTF-8"?>
<book id="42">
<title>Walden</title>
<authorName>Henry David Thoreau</authorName>
</book>
class BookController {
def updateBook(Book book) {
// The book will have been retrieved from the database and updated
// by doing something like this:
//
// book == Book.get('42')
// if(book != null) {
// book.properties = request
// }
//
// the code above represents what the framework will
// have done. There is no need to write that code.
// ...
}
}
数据绑定取决于由 DataBindingSourceCreator
接口的实例创建的 DataBindingSource
接口的实例。特定实现 DataBindingSourceCreator
将根据请求的 contentType
选择。提供的多个实现用于处理常见内容类型。对于大多数用例来说,默认实现已经足够。下表列出了核心框架支持的内容类型以及针对每个内容类型使用 DataBindingSourceCreator
实现。所有实现类都在 org.grails.databinding.bindingsource
包中。
内容类型 | Bean 名称 | DataBindingSourceCreator 实现 |
---|---|---|
application/xml、text/xml |
xmlDataBindingSourceCreator |
XmlDataBindingSourceCreator |
application/json、text/json |
jsonDataBindingSourceCreator |
JsonDataBindingSourceCreator |
application/hal+json |
halJsonDataBindingSourceCreator |
HalJsonDataBindingSourceCreator |
application/hal+xml |
halXmlDataBindingSourceCreator |
HalXmlDataBindingSourceCreator |
为了针对任何这些内容类型提供您自己的 DataBindingSourceCreator
,请编写实现 DataBindingSourceCreator
的类并在 Spring 应用程序上下文中注册该类的实例。如果您正在替换现有的一个帮助器,请使用上述对应的 bean 名称。如果您正在针对核心框架无法处理的内容类型提供帮助器,则 bean 名称可以是您喜欢的任何名称,但您应注意不要与上述任何一个 bean 名称冲突。
DataBindingSourceCreator
接口仅定义 2 个方法
package org.grails.databinding.bindingsource
import grails.web.mime.MimeType
import grails.databinding.DataBindingSource
/**
* A factory for DataBindingSource instances
*
* @since 2.3
* @see DataBindingSourceRegistry
* @see DataBindingSource
*
*/
interface DataBindingSourceCreator {
/**
* `return All of the {`link MimeType} supported by this helper
*/
MimeType[] getMimeTypes()
/**
* Creates a DataBindingSource suitable for binding bindingSource to bindingTarget
*
* @param mimeType a mime type
* @param bindingTarget the target of the data binding
* @param bindingSource the value being bound
* @return a DataBindingSource
*/
DataBindingSource createDataBindingSource(MimeType mimeType, Object bindingTarget, Object bindingSource)
}
AbstractRequestBodyDataBindingSourceCreator 是一个抽象类,旨在扩展来简化编写自定义 DataBindingSourceCreator
类。扩展 AbstractRequestbodyDatabindingSourceCreator
的类需要实现一个名为 createBindingSource
的方法,该方法接受 InputStream
作为参数,并返回 DataBindingSource
以及实现上面 DataBindingSourceCreator
接口中描述的 getMimeTypes
方法。createBindingSource
的 InputStream
参数提供对请求主体的访问权。
以下代码显示了一个简单实现。
package com.demo.myapp.databinding
import grails.web.mime.MimeType
import grails.databinding.DataBindingSource
import org...databinding.SimpleMapDataBindingSource
import org...databinding.bindingsource.AbstractRequestBodyDataBindingSourceCreator
/**
* A custom DataBindingSourceCreator capable of parsing key value pairs out of
* a request body containing a comma separated list of key:value pairs like:
*
* name:Herman,age:99,town:STL
*
*/
class MyCustomDataBindingSourceCreator extends AbstractRequestBodyDataBindingSourceCreator {
@Override
public MimeType[] getMimeTypes() {
[new MimeType('text/custom+demo+csv')] as MimeType[]
}
@Override
protected DataBindingSource createBindingSource(InputStream inputStream) {
def map = [:]
def reader = new InputStreamReader(inputStream)
// this is an obviously naive parser and is intended
// for demonstration purposes only.
reader.eachLine { line ->
def keyValuePairs = line.split(',')
keyValuePairs.each { keyValuePair ->
if(keyValuePair?.trim()) {
def keyValuePieces = keyValuePair.split(':')
def key = keyValuePieces[0].trim()
def value = keyValuePieces[1].trim()
map<<key>> = value
}
}
}
// create and return a DataBindingSource which contains the parsed data
new SimpleMapDataBindingSource(map)
}
}
MyCustomDataSourceCreator
的实例需要在 Spring 应用程序上下文中注册。
beans = {
myCustomCreator com.demo.myapp.databinding.MyCustomDataBindingSourceCreator
// ...
}
有了它,每当需要 DataBindingSourceCreator
来处理 contentType
为 "text/custom+demo+csv" 的请求时,框架都会使用 myCustomCreator
bean。
9.12 RSS 和 Atom
Grails 内没有为 RSS 或 Atom 提供直接支持。你可以使用 render 方法的 XML 功能构建 RSS 或 ATOM 提要。