(快速參考)

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 轉換新增到任何網域類別來完成

import grails.rest.*

@Resource(uri='/books')
class Book {

    String title

    static constraints = {
        title blank:false
    }
}

只要新增 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.groovygrails.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 {
    ...
}

在這種情況下,POSTPUTDELETE 要求將會被禁止。

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 介面,則只需要 createedit 動作。

例如,如果您有 巢狀資源,則通常會想要查詢父代和子代識別碼。例如,給定下列 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 類別包含執行資料繫結的程式碼,例如 saveupdate 等動作。此類別定義一個 getObjectToBind() 方法,用於傳回一個值,此值將作為資料繫結的來源。例如,更新動作會執行類似下列的動作…​

class RestfulController<T> {

    def update() {
        T instance = // retrieve instance from the database...

        instance.properties = getObjectToBind()

        // ...
    }

    // ...
}

預設情況下,getObjectToBind() 方法會傳回 request 物件。當 request 物件用作繫結來源時,如果要求有內文,則會剖析內文,並使用其內容執行資料繫結,否則會使用要求參數執行資料繫結。RestfulController 的子類別可以覆寫 getObjectToBind() 方法,並傳回任何有效的繫結來源,包括 MapDataBindingSource。在多數使用案例中,繫結要求是適當的,但 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 介面,則 createedit 動作是必需的。它們用於呈現適當的 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 文件中。

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 方法

创建 HTTP 客户端
    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.xmlgrails.rest.render.json 套件中找到。這些套件預設使用 Grails 轉換器(grails.converters.XMLgrails.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

為什麼轉換器和呈現器之間會有區別?好吧,呈現器有更大的彈性來使用您選擇的任何呈現技術。在實作自訂呈現器時,您可以使用 JacksonGson 或任何 Java 函式庫來實作呈現器。另一方面,轉換器與 Grails 本身的封送實作有非常密切的關聯。

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 方法。createBindingSourceInputStream 引數提供存取要求主體的功能。

以下程式碼顯示一個簡單的實作。

src/main/groovy/com/demo/myapp/databinding/MyCustomDataBindingSourceCreator.groovy
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 應用程式內容中註冊。

grails-app/conf/spring/resources.groovy
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 饋送。