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 https://127.0.0.1:8080/books/1,它會產生類似這樣的回應
<?xml version="1.0" encoding="UTF-8"?>
<book id="1">
<title>The Stand</title>
</book>
如果您將 URL 變更為 https://127.0.0.1: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 資源對應來建立強大的網路介面時很有用。若要讓連結觸發這種類型的事件,或許可以擷取所有具有 data-method
屬性的連結的按一下事件,並透過 JavaScript 發出表單提交。
9.4 REST 資源版本化
REST API 的常見需求是同時公開不同版本。在 Grails 中有幾種方法可以達成這個目標。
使用 URI 版本化
一個常見的方法是使用 URI 來版本化 API(儘管這個方法不建議使用,而建議使用 Hypermedia)。例如,你可以定義下列 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'
}
這個方法的缺點是需要兩個不同的 URI 名稱空間給你的 API。
使用 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 https://127.0.0.1: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",
...
}
將新的 Mime 類型放在 'all' 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://127.0.0.1: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()
方法允許在需要時變更該行為。
使用自訂的 RestfulController 子類別與 Resource 標記
您也可以自訂支援 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 動作的關鍵在於 Grails 2.3 中引入的 respond 方法。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
進入動作。如果傳遞 null
,則 respond
方法會傳回 404 錯誤,否則它會再次嘗試呈現適當的回應。如果格式為 HTML,則會產生適當的模型。下列動作在功能上等同於上述動作
def show(Book book) {
if(book == null) {
render status:404
}
else {
return [book: book]
}
}
實作 'save' 動作
save
動作會建立新的資源表示。首先,只要定義一個動作,接受資源作為第一個參數,並使用 grails.gorm.transactions.Transactional
轉換將其標記為 Transactional
@Transactional
def save(Book book) {
...
}
接著,第一件事是檢查資源是否有任何 驗證錯誤,如果有,則回應錯誤
if(book.hasErrors()) {
respond book.errors, view:'create'
}
else {
...
}
如果是 HTML,則會再次呈現「建立」檢視,以便使用者可以修正無效的輸入。如果是其他格式(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,則會再次呈現「編輯」檢視,以便使用者可以修正無效的輸入。如果是其他格式(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
動作會刪除現有的資源。實作與 update
動作非常類似,只不過呼叫的是 delete()
方法
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 控制器
若要查看這些概念中的一些實際應用並協助您開始使用,鷹架外掛程式 2.0 版以上可以為您產生準備好的 REST 控制器,只要執行指令
$ grails generate-controller <<Domain Class Name>>
9.6 使用 HttpClient 呼叫 REST 服務
使用 Micronaut HTTP Client 呼叫 Grails REST 服務(以及第三方服務)非常簡單。此 HTTP Client 同時具有低階 API 和較高階的 AOP 驅動 API,因此對於簡單的請求以及建立宣告式、類型安全的 API 層都很有用。
若要使用 Micronaut HTTP 客户端,您的 classpath 中必须有 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 |
查阅 Http Client 部分 以获取有关使用 HttpClient
低阶 API 的更多信息。
声明式 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)
}
有关编写和使用声明式客户端的更多详细信息,请参阅 Http Client 部分。
9.7 REST 配置文件
自 Grails 3.1 起,Grails 支持为创建 REST 应用程序而定制的配置文件,该配置文件提供了一组更集中的依赖项和命令。
開始使用 REST API 類型的應用程式
$ grails create-restapi my-api
這將建立一個新的 REST 應用程式,提供以下功能
-
用於建立和產生 REST 端點的預設指令集
-
預設使用 JSON 檢視來呈現回應(請參閱下一節)
-
外掛比預設的 Grails 網頁樣式應用程式少(沒有 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"
只有在將 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 以來,Grails 團隊認為 JSON 檢視是為客戶端呈現 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
中的 bean 即可。
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 Server Pages (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,是 Hypermedia as the Engine of Application State 的縮寫,是一種常見的模式,適用於使用超媒體和連結來定義 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-app/conf/spring/resources.groovy
中的 grails.rest.render.hal.HalJsonRenderer
(或 XML 變體的 HalXmlRenderer
)實例覆寫呈現器
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://127.0.0.1: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": "https://127.0.0.1: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-app/conf/spring/resources.groovy
中的 grails.rest.render.hal.HalJsonCollectionRenderer
實例覆寫呈現器
import grails.rest.render.hal.*
beans = {
halBookCollectionRenderer(HalJsonCollectionRenderer, rest.test.Book)
}
有了這個 bean,要求 HAL 內容類型將會傳回 HAL
$ curl -i -H "Accept: application/hal+json" http://127.0.0.1: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": "https://127.0.0.1:8080/books",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"book": [
{
"_links": {
"self": {
"href": "https://127.0.0.1:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "The Stand"
},
{
"_links": {
"self": {
"href": "https://127.0.0.1:8080/books/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Infinite Jest"
},
{
"_links": {
"self": {
"href": "https://127.0.0.1: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://127.0.0.1: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": "https://127.0.0.1:8080/books",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"publications": [
{
"_links": {
"self": {
"href": "https://127.0.0.1:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "The Stand"
},
{
"_links": {
"self": {
"href": "https://127.0.0.1:8080/books/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Infinite Jest"
},
{
"_links": {
"self": {
"href": "https://127.0.0.1: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",
...
]
將新的 Mime 類型放在 'all' 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 定義單一書籍實例的 HAL 呈現器,傳回 Mime 類型 application/vnd.books.org.book+json
。第二個 bean 定義用於呈現書籍集合的 Mime 類型(在本例中為 application/vnd.books.org.booklist+json
)
application/vnd.books.org.booklist+json 是媒體範圍的範例(http://www.w3.org/Protocols/rfc2616/rfc2616.html - 標頭欄位定義)。此範例使用實體(書籍)和操作(清單)來形成媒體範圍值,但實際上,可能不需要為每個操作建立個別的 Mime 類型。此外,可能不需要在實體層級建立 Mime 類型。請參閱「版本化 REST 資源」部分,以進一步瞭解如何定義自己的 Mime 類型。
|
有了這個,發出對新 Mime 類型的要求會傳回必要的 HAL
$ curl -i -H "Accept: application/vnd.books.org.book+json" https://127.0.0.1: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": "https://127.0.0.1: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": "https://127.0.0.1:8080/books/1",
"hreflang": "en",
"type": "application/vnd.books.org.book+json"
}
"publisher": {
"href": "https://127.0.0.1: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 新資源時發生驗證錯誤,錯誤物件將會傳送回傳,並允許使用 422 回應碼
$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d "" http://127.0.0.1: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://127.0.0.1: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": "https://127.0.0.1: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.
// ...
}
}
資料繫結仰賴 DataBindingSource 介面的執行個體,而該執行個體是由 DataBindingSourceCreator 介面的執行個體所建立。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
來處理具有「text/custom+demo+csv」contentType
的要求時,架構就會使用 myCustomCreator
bean。
9.12 RSS 和 Atom
Grails 中未提供對 RSS 或 Atom 的直接支援。您可以使用 render 方法的 XML 功能來建構 RSS 或 ATOM 饋送。