(快速參考)

7 網頁層

版本 6.2.0

7 網頁層

7.1 控制器

控制器處理要求並建立或準備回應。控制器可以直接產生回應或委派給檢視。若要建立控制器,只需建立一個類別,其名稱在 grails-app/controllers 目錄中以 Controller 結尾(如果在套件中,則在子目錄中)。

預設的 URL 對應 組態確保控制器名稱的第一部分會對應到 URI,且在控制器中定義的每個動作會對應到控制器名稱 URI 中的 URI。

7.1.1 了解控制器和動作

建立控制器

可以使用 create-controllergenerate-controller 指令建立控制器。例如,請嘗試從 Grails 專案的根目錄執行下列指令

grails create-controller book

此指令會在 grails-app/controllers/myapp/BookController.groovy 位置建立一個控制器

package myapp

class BookController {

    def index() { }
}

其中「myapp」會是應用程式的名稱,如果未指定,則為預設的套件名稱。

BookController 預設會對應到 /book URI(相對於應用程式根目錄)。

create-controllergenerate-controller 指令僅供方便使用,你也可以使用你最喜歡的文字編輯器或 IDE 建立控制器

建立動作

控制器可以有多個公開的動作方法;每個動作會對應到一個 URI

class BookController {

    def list() {

        // do controller logic
        // create model

        return model
    }
}

這個範例會對應到 /book/list URI,因為屬性的名稱為 list

預設動作

控制器有一個預設 URI 的概念,會對應到控制器的根目錄 URI,例如 BookController/book。當要求預設 URI 時,會呼叫的動作是由下列規則決定

  • 如果只有一個動作,則為預設動作

  • 如果你有一個名為 index 的動作,則為預設動作

  • 或者,您可以使用 defaultAction 屬性明確設定它

static defaultAction = "list"

7.1.2 控制器和範圍

可用範圍

範圍是類似雜湊的物件,您可以在其中儲存變數。控制器可以使用下列範圍

  • servletContext - 也稱為應用程式範圍,此範圍讓您可以在整個 Web 應用程式中分享狀態。servletContext 是 ServletContext 的執行個體

  • session - 會話允許將狀態與特定使用者關聯,且通常使用 Cookie 將會話與客戶端關聯。session 物件是 HttpSession 的執行個體

  • request - 要求物件只允許儲存目前要求的物件。request 物件是 HttpServletRequest 的執行個體

  • params - 可變動的內建要求查詢字串或 POST 參數

  • flash - 詳見下方

存取範圍

範圍可以使用上述變數名稱搭配 Groovy 的陣列索引運算子存取,即使是在 Servlet API 提供的類別(例如 HttpServletRequest)上也是如此

class BookController {
    def find() {
        def findBy = params["findBy"]
        def appContext = request["foo"]
        def loggedUser = session["logged_user"]
    }
}

您也可以使用去參考運算子存取範圍內的數值,讓語法更為清楚

class BookController {
    def find() {
        def findBy = params.findBy
        def appContext = request.foo
        def loggedUser = session.logged_user
    }
}

這是 Grails 統一存取不同範圍的方法之一。

使用 Flash 範圍

Grails 支援 flash 範圍的概念,作為暫時儲存空間,讓屬性僅在目前和下一個要求中可用。之後,屬性會被清除。這對於在重新導向之前直接設定訊息很有用,例如

def delete() {
    def b = Book.get(params.id)
    if (!b) {
        flash.message = "User not found for id ${params.id}"
        redirect(action:list)
    }
    ... // remaining code
}

當要求 delete 動作時,message 數值會在範圍內,且可用於顯示資訊訊息。它會在第二次要求後從 flash 範圍中移除。

請注意,屬性名稱可以是您想要的任何名稱,且數值通常是用於顯示訊息的字串,但可以是任何物件類型。

範圍控制器

新建立的應用程式會在 application.yml 中將 grails.controllers.defaultScope 屬性設定為「singleton」數值。您可以將此數值變更為下列支援的範圍之一。如果屬性完全未指定數值,控制器會預設為「prototype」範圍。

支援的控制器範圍為

  • prototype(預設) - 會為每個要求建立新的控制器(建議用於 Closure 屬性的動作)

  • session - 會為使用者會話的範圍建立一個控制器

  • singleton - 控制器只存在一個執行個體(建議用於方法的動作)

若要啟用其中一個範圍,請將靜態 scope 屬性新增至您的類別,並指定上述有效的範圍數值,例如

static scope = "singleton"

您可以使用 grails.controllers.defaultScope 鍵在 application.yml 中定義預設策略,例如

grails:
    controllers:
        defaultScope: singleton
明智地使用範圍控制器。例如,我們不建議在 singleton 範圍的控制器中放置任何屬性,因為它們會與所有要求分享。

7.1.3 模型和檢視

傳回模型

模型是檢視在呈現時使用的 Map。該 Map 中的鍵對應於檢視可存取的變數名稱。有幾種方式可傳回模型。首先,您可以明確傳回 Map 實例

def show() {
    [book: Book.get(params.id)]
}
以上內容反映您應在架構檢視中使用什麼 - 請參閱架構區段以取得更多詳細資訊。

更進階的方法是傳回 Spring ModelAndView 類別的實例

import org.springframework.web.servlet.ModelAndView

def index() {
    // get some books just for the index page, perhaps your favorites
    def favoriteBooks = ...

    // forward to the list view to show them
    return new ModelAndView("/book/list", [ bookList : favoriteBooks ])
}

需要注意的一件事是,某些變數名稱無法在您的模型中使用

  • 屬性

  • 應用程式

目前,如果您使用它們,不會報告任何錯誤,但希望這會在 Grails 的未來版本中有所改變。

選擇檢視

在前兩個範例中,都沒有程式碼指定要呈現哪個檢視。那麼 Grails 如何知道要選擇哪一個?答案在於慣例。Grails 會在位置 grails-app/views/book/show.gsp 尋找此 show 動作的檢視

class BookController {
    def show() {
         [book: Book.get(params.id)]
    }
}

若要呈現不同的檢視,請使用render方法

def show() {
    def map = [book: Book.get(params.id)]
    render(view: "display", model: map)
}

在這種情況下,Grails 會嘗試在位置 grails-app/views/book/display.gsp 呈現檢視。請注意,Grails 會自動使用 grails-app/views 目錄的 book 目錄限定檢視位置。這很方便,但要存取共用檢視,請使用絕對路徑,而不是相對路徑

def show() {
    def map = [book: Book.get(params.id)]
    render(view: "/shared/display", model: map)
}

在這種情況下,Grails 會嘗試在位置 grails-app/views/shared/display.gsp 呈現檢視。

Grails 也支援 JSP 作為檢視,因此如果在預期位置找不到 GSP,但找到 JSP,則會改用 JSP。

與 GSP 不同,JSP 必須位於目錄路徑 /src/main/webapp/WEB-INF/grails-app/views 中。

此外,為確保 JSP 按預期運作,別忘了在 build.gradle 檔案中包含 JSP 和 JSTL 實作所需的相依性。

為命名空間控制器選擇檢視

如果控制器使用命名空間屬性為自己定義命名空間,這將影響 Grails 尋找使用相對路徑指定的檢視的根目錄。命名空間控制器呈現的檢視的預設根目錄為 grails-app/views/<命名空間名稱>/<控制器名稱>/。如果在命名空間目錄中找不到檢視,則 Grails 會改為在非命名空間目錄中尋找檢視。

請參閱以下範例。

class ReportingController {
    static namespace = 'business'

    def humanResources() {
        // This will render grails-app/views/business/reporting/humanResources.gsp
        // if it exists.

        // If grails-app/views/business/reporting/humanResources.gsp does not
        // exist the fallback will be grails-app/views/reporting/humanResources.gsp.

        // The namespaced GSP will take precedence over the non-namespaced GSP.

        [numberOfEmployees: 9]
    }


    def accountsReceivable() {
        // This will render grails-app/views/business/reporting/numberCrunch.gsp
        // if it exists.

        // If grails-app/views/business/reporting/numberCrunch.gsp does not
        // exist the fallback will be grails-app/views/reporting/numberCrunch.gsp.

        // The namespaced GSP will take precedence over the non-namespaced GSP.

        render view: 'numberCrunch', model: [numberOfEmployees: 13]
    }
}

呈現回應

有時(例如使用 Ajax 應用程式)直接從控制器將文字或程式碼片段呈現到回應比較容易。為此,可以使用高度彈性的 render 方法

render "Hello World!"

上述程式碼會將文字「Hello World!」寫入回應。其他範例包括

// write some markup
render {
   for (b in books) {
      div(id: b.id, b.title)
   }
}
// render a specific view
render(view: 'show')
// render a template for each item in a collection
render(template: 'book_template', collection: Book.list())
// render some text with encoding and content type
render(text: "<xml>some xml</xml>", contentType: "text/xml", encoding: "UTF-8")

如果您計畫使用 Groovy 的 MarkupBuilderrender 方法產生 HTML,請小心 HTML 元素和 Grails 標籤之間的命名衝突,例如

import groovy.xml.MarkupBuilder
...
def login() {
    def writer = new StringWriter()
    def builder = new MarkupBuilder(writer)
    builder.html {
        head {
            title 'Log in'
        }
        body {
            h1 'Hello'
            form {
            }
        }
    }

    def html = writer.toString()
    render html
}

這實際上會 呼叫 form 標籤(這會傳回一些文字,而 MarkupBuilder 會忽略這些文字)。若要正確輸出 <form> 元素,請使用下列方法

def login() {
    // ...
    body {
        h1 'Hello'
        builder.form {
        }
    }
    // ...
}

7.1.4 重新導向和串連

重新導向

可以使用 redirect 控制器方法重新導向動作

class OverviewController {

    def login() {}

    def find() {
        if (!session.user)
            redirect(action: 'login')
            return
        }
        ...
    }
}

在內部,redirect 方法會使用 HttpServletResponse 物件的 sendRedirect 方法。

redirect 方法會預期下列其中一項

  • 動作名稱(以及控制器名稱,如果重新導向不是到目前控制器中的動作)

// Also redirects to the index action in the home controller
redirect(controller: 'home', action: 'index')
  • 相對於應用程式內容路徑的資源 URI

// Redirect to an explicit URI
redirect(uri: "/login.html")
  • 或完整 URL

// Redirect to a URL
redirect(url: "http://grails.org")
// Redirect to the domain instance
Book book = ... // obtain a domain instance
redirect book

在上述範例中,Grails 會使用網域類別 id(如果存在)建構連結。

可以選擇使用該方法的 params 引數,從一個動作傳遞參數到下一個動作

redirect(action: 'myaction', params: [myparam: "myvalue"])

這些參數可透過 params 動態屬性取得,該屬性會存取要求參數。如果參數指定的名稱與要求參數相同,則會覆寫要求參數,並使用控制器參數。

由於 params 物件是 Map,因此您可以使用它將目前的請求參數從一個動作傳遞到下一個動作

redirect(action: "next", params: params)

最後,您也可以在目標 URI 中包含片段

redirect(controller: "test", action: "show", fragment: "profile")

這會(根據 URL 對應)重新導向到類似「/myapp/test/show#profile」的內容。

串連

動作也可以串連。串連允許將模型從一個動作保留到下一個動作。例如,呼叫此動作中的 first 動作

class ExampleChainController {

    def first() {
        chain(action: second, model: [one: 1])
    }

    def second () {
        chain(action: third, model: [two: 2])
    }

    def third() {
        [three: 3])
    }
}

結果在模型中

[one: 1, two: 2, three: 3]

模型可以在鏈中的後續控制器動作中使用 chainModel 地圖存取。此動態屬性僅存在於呼叫 chain 方法後的動作中

class ChainController {

    def nextInChain() {
        def model = chainModel.myModel
        ...
    }
}

redirect 方法類似,您也可以將參數傳遞給 chain 方法

chain(action: "action1", model: [one: 1], params: [myparam: "param1"])
鏈方法使用 HTTP 會話,因此僅應在應用程式有狀態時使用。

7.1.5 資料繫結

資料繫結是將輸入的請求參數「繫結」到物件屬性或整個物件圖形上的動作。資料繫結應處理所有必要的類型轉換,因為請求參數(通常透過表單提交傳送)始終為字串,而 Groovy 或 Java 物件的屬性可能不是。

基於 Map 的繫結

資料繫結器能夠將 Map 中的值轉換並指定給物件的屬性。繫結器會使用 Map 中的鍵將 Map 中的項目關聯到物件的屬性,這些鍵的值對應於物件上的屬性名稱。下列程式碼示範了基礎知識

grails-app/domain/Person.groovy
class Person {
    String firstName
    String lastName
    Integer age
}
def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63]

def person = new Person(bindingMap)

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63

若要更新網域物件的屬性,您可以將 Map 指定給網域類別的 properties 屬性

def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63]

def person = Person.get(someId)
person.properties = bindingMap

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63

繫結器可以使用 Map of Maps 填入完整的物件圖形。

class Person {
    String firstName
    String lastName
    Integer age
    Address homeAddress
}

class Address {
    String county
    String country
}
def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63, homeAddress: [county: 'Surrey', country: 'England'] ]

def person = new Person(bindingMap)

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63
assert person.homeAddress.county == 'Surrey'
assert person.homeAddress.country == 'England'

繫結到集合和 Map

資料繫結器可以填入和更新集合和 Map。下列程式碼顯示了在網域類別中填入物件 List 的簡單範例

class Band {
    String name
    static hasMany = [albums: Album]
    List albums
}

class Album {
    String title
    Integer numberOfTracks
}
def bindingMap = [name: 'Genesis',
                  'albums[0]': [title: 'Foxtrot', numberOfTracks: 6],
                  'albums[1]': [title: 'Nursery Cryme', numberOfTracks: 7]]

def band = new Band(bindingMap)

assert band.name == 'Genesis'
assert band.albums.size() == 2
assert band.albums[0].title == 'Foxtrot'
assert band.albums[0].numberOfTracks == 6
assert band.albums[1].title == 'Nursery Cryme'
assert band.albums[1].numberOfTracks == 7

如果 albums 是陣列而非 List,該程式碼將以相同的方式運作。

請注意,當繫結到 Set 時,繫結到 SetMap 的結構與繫結到 ListMap 的結構相同,但由於 Set 是未排序的,因此索引不一定要與 Set 中元素的順序對應。在上述程式碼範例中,如果 albumsSet 而不是 List,則 bindingMap 可以看起來完全相同,但「Foxtrot」可能是 Set 中的第一個專輯,也可能是第二個。當更新 Set 中的現有元素時,指定給 SetMap 中必須包含 id 元素,這些元素代表正在更新的 Set 中的元素,如下列範例所示

/*
 * The value of the indexes 0 and 1 in albums[0] and albums[1] are arbitrary
 * values that can be anything as long as they are unique within the Map.
 * They do not correspond to the order of elements in albums because albums
 * is a Set.
 */
def bindingMap = ['albums[0]': [id: 9, title: 'The Lamb Lies Down On Broadway']
                  'albums[1]': [id: 4, title: 'Selling England By The Pound']]

def band = Band.get(someBandId)

/*
 * This will find the Album in albums that has an id of 9 and will set its title
 * to 'The Lamb Lies Down On Broadway' and will find the Album in albums that has
 * an id of 4 and set its title to 'Selling England By The Pound'.  In both
 * cases if the Album cannot be found in albums then the album will be retrieved
 * from the database by id, the Album will be added to albums and will be updated
 * with the values described above.  If a Album with the specified id cannot be
 * found in the database, then a binding error will be created and associated
 * with the band object.  More on binding errors later.
 */
band.properties = bindingMap

當繫結至 Map 時,繫結 Map 的結構與用於繫結至 ListSetMap 結構相同,且方括號內的索引對應於繫結至的 Map 中的鍵。請參閱下列程式碼

class Album {
    String title
    static hasMany = [players: Player]
    Map players
}

class Player {
    String name
}
def bindingMap = [title: 'The Lamb Lies Down On Broadway',
                  'players[guitar]': [name: 'Steve Hackett'],
                  'players[vocals]': [name: 'Peter Gabriel'],
                  'players[keyboards]': [name: 'Tony Banks']]

def album = new Album(bindingMap)

assert album.title == 'The Lamb Lies Down On Broadway'
assert album.players.size() == 3
assert album.players.guitar.name == 'Steve Hackett'
assert album.players.vocals.name == 'Peter Gabriel'
assert album.players.keyboards.name == 'Tony Banks'

更新現有 Map 時,如果繫結 Map 中指定的鍵不存在於繫結至的 Map 中,則會建立一個新值並將其新增至 Map,其鍵如下列範例所示

def bindingMap = [title: 'The Lamb Lies Down On Broadway',
                  'players[guitar]': [name: 'Steve Hackett'],
                  'players[vocals]': [name: 'Peter Gabriel'],
                  'players[keyboards]': [name: 'Tony Banks']]

def album = new Album(bindingMap)

assert album.title == 'The Lamb Lies Down On Broadway'
assert album.players.size() == 3
assert album.players.guitar.name == 'Steve Hackett'
assert album.players.vocals.name  == 'Peter Gabriel'
assert album.players.keyboards.name  == 'Tony Banks'

def updatedBindingMap = ['players[drums]': [name: 'Phil Collins'],
                         'players[keyboards]': [name: 'Anthony George Banks']]

album.properties = updatedBindingMap

assert album.title == 'The Lamb Lies Down On Broadway'
assert album.players.size() == 4
assert album.players.guitar.name == 'Steve Hackett'
assert album.players.vocals.name == 'Peter Gabriel'
assert album.players.keyboards.name == 'Anthony George Banks'
assert album.players.drums.name == 'Phil Collins'

將要求資料繫結至模型

控制器中可用的 params 物件具有特殊行為,可協助將點狀要求參數名稱轉換為嵌套式 Map,以便資料繫結器使用。例如,如果要求包含名稱為 person.homeAddress.countryperson.homeAddress.city 的要求參數,其值分別為 'USA' 和 'St. Louis',則 params 會包含下列項目

[person: [homeAddress: [country: 'USA', city: 'St. Louis']]]

有兩種方法可將要求參數繫結至網域類別的屬性。第一種方法是使用網域類別的 Map 建構函數

def save() {
    def b = new Book(params)
    b.save()
}

資料繫結發生在 new Book(params) 程式碼中。透過將 params 物件傳遞給網域類別建構函數,Grails 會自動辨識您嘗試從要求參數繫結。因此,如果我們有類似下列的輸入要求

/book/save?title=The%20Stand&author=Stephen%20King

titleauthor 要求參數會自動設定至網域類別。您可以使用 properties 屬性來對現有執行個體執行資料繫結

def save() {
    def b = Book.get(params.id)
    b.properties = params
    b.save()
}

這與使用隱式建構函數具有相同效果。

繫結空字串(不含任何字元,甚至連空白都不含)時,資料繫結器會將空字串轉換為 null。這簡化了最常見的情況,也就是將空表單欄位視為 null 值,因為沒有辦法將 null 實際提交為要求參數。如果這種行為不理想,應用程式可以自行指定值。

預設情況下,大量屬性繫結機制會在繫結時自動移除所有字串的空白。若要停用此行為,請在 grails-app/conf/application.groovy 中將 grails.databinding.trimStrings 屬性設為 false。

// the default value is true
grails.databinding.trimStrings = false

// ...

預設情況下,大量屬性繫結機制會在繫結時自動將所有空字串轉換為 null。若要停用此行為,請在 grails-app/conf/application.groovy 中將 grails.databinding.convertEmptyStringsToNull 屬性設為 false。

// the default value is true
grails.databinding.convertEmptyStringsToNull = false

// ...

事件順序為先移除字串空白,再進行 null 轉換,因此如果 trimStringstrue,而 convertEmptyStringsToNulltrue,不僅空字串會轉換為 null,空白字串也會轉換。空白字串是指任何 trim() 方法會傳回空字串的字串。

Grails 中的這些資料繫結形式非常方便,但也不分青紅皂白。換句話說,它們會繫結目標物件的所有非暫時性、已輸入型別的執行個體屬性,包括您可能不希望繫結的屬性。即使 UI 中的表單未提交所有屬性,攻擊者仍可透過原始 HTTP 要求傳送惡意資料。所幸的是,Grails 也讓您能輕鬆防範此類攻擊,請參閱標題為「資料繫結和安全性疑慮」的章節以取得更多資訊。

資料繫結和單端關聯

如果您有 one-to-onemany-to-one 關聯,您可以使用 Grails 的資料繫結功能來更新這些關聯。例如,如果您有下列這類的傳入要求

/book/save?author.id=20

Grails 會自動偵測要求參數上的 .id 字尾,並在執行資料繫結時尋找給定 id 的 Author 執行個體,例如

def b = new Book(params)

關聯屬性可以透過傳遞文字 String「null」設為 null。例如

/book/save?author.id=null

資料繫結和多端關聯

如果您有一個 one-to-many 或 many-to-many 關聯,則有不同的資料繫結技術,具體取決於關聯類型。

如果您有一個基於 Set 的關聯(hasMany 的預設值),則填入關聯最簡單的方法是傳送識別碼清單。例如,請考慮以下 <g:select> 的使用方式

<g:select name="books"
          from="${Book.list()}"
          size="5" multiple="yes" optionKey="id"
          value="${author?.books}" />

這會產生一個選取方塊,讓您選取多個值。在此情況下,如果您提交表單,Grails 會自動使用選取方塊中的識別碼來填入 books 關聯。

然而,如果您有要更新關聯物件的屬性的場景,此技術將無法使用。您改用下標運算子

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />

然而,使用基於 Set 的關聯,您必須按照您計畫進行更新的順序來呈現標記。這是因為 Set 沒有順序的概念,所以儘管我們參考 books[0]books[1],但除非您自己套用一些明確的排序,否則無法保證關聯的順序在伺服器端會正確。

如果您使用基於 List 的關聯,這不會是個問題,因為 List 有定義的順序和您可以參考的索引。這也適用於基於 Map 的關聯。

另請注意,如果您要繫結的關聯大小為 2,而您參考的元素在關聯大小之外

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[2].title" value="Red Madder" />

那麼 Grails 會自動在定義的位置為您建立新的執行個體。

您可以使用與單端關聯相同的 .id 語法,將關聯類型的現有執行個體繫結到 List。例如

<g:select name="books[0].id" from="${bookList}"
          value="${author?.books[0]?.id}" />

<g:select name="books[1].id" from="${bookList}"
          value="${author?.books[1]?.id}" />

<g:select name="books[2].id" from="${bookList}"
          value="${author?.books[2]?.id}" />

將允許在 books List 中個別選取個別項目。

也可以用相同的方式移除特定索引的項目。例如

<g:select name="books[0].id"
          from="${Book.list()}"
          value="${author?.books[0]?.id}"
          noSelection="['null': '']"/>

將會呈現一個選取方塊,如果選取空選項,將會移除 books[0] 的關聯。

繫結到 Map 屬性的方式相同,除了參數名稱中的清單索引會替換為地圖金鑰

<g:select name="images[cover].id"
          from="${Image.list()}"
          value="${book?.images[cover]?.id}"
          noSelection="['null': '']"/>

這會將選取的圖片繫結到 Map 屬性 images,其金鑰為 "cover"

繫結到 Maps、陣列和集合時,資料繫結器會自動根據需要增加集合的大小。

繫結器將增加集合大小的預設限制為 256。如果資料繫結器遇到需要將集合增加到超過此限制的項目,該項目將會被忽略。此限制可以透過在 application.groovy 中將值指定給 grails.databinding.autoGrowCollectionLimit 屬性來設定。
grails-app/conf/application.groovy
// the default value is 256
grails.databinding.autoGrowCollectionLimit = 128

// ...

使用多個網域類別進行資料繫結

可以從 params 物件將資料繫結到多個網域物件。

例如,您有傳入的請求為

/book/save?book.title=The%20Stand&author.name=Stephen%20King

您會注意到與上述請求的不同之處在於,每個參數都有前綴,例如 author.book.,用於隔離哪些參數屬於哪種類型。Grails 的 params 物件就像一個多維雜湊,您可以對其編制索引,以隔離僅要繫結的參數子集。

def b = new Book(params.book)

請注意我們如何使用 book.title 參數的第一個點之前的字首,以隔離僅在此層級以下的參數以進行繫結。我們可以對 Author 網域類別執行相同的操作

def a = new Author(params.author)

資料繫結與動作引數

控制器動作引數會受到請求參數資料繫結影響。控制器動作引數有 2 個類別。第一個類別是命令物件。複雜類型會被視為命令物件。請參閱使用者指南的 命令物件 區段以取得詳細資料。另一個類別是基本物件類型。支援的類型有 8 個基本類型、其對應的類型包裝器和 java.lang.String。預設行為是依名稱將請求參數對應到動作引數

class AccountingController {

   // accountNumber will be initialized with the value of params.accountNumber
   // accountType will be initialized with params.accountType
   def displayInvoice(String accountNumber, int accountType) {
       // ...
   }
}

對於基本引數和任何基本類型包裝器類別的實例引數,在將請求參數值繫結到動作引數之前,必須進行類型轉換。類型轉換會自動進行。在如上所示的範例中,params.accountType 請求參數必須轉換為 int。如果類型轉換因任何原因失敗,引數將會具有其預設值,依據正常的 Java 行為(類型包裝器參考為 null、布林值為 false、數字為零),且對應的錯誤將會新增到定義控制器的 errors 屬性中。

/accounting/displayInvoice?accountNumber=B59786&accountType=bogusValue

由於無法將「bogusValue」轉換為 int 類型,accountType 的值將會為零,控制器的 errors.hasErrors() 將會為 true,控制器的 errors.errorCount 將會等於 1,且控制器的 errors.getFieldError('accountType') 將會包含對應的錯誤。

如果引數名稱與請求參數的名稱不符,則可以將 @grails.web.RequestParameter 注解套用至引數,以表達應繫結到該引數的請求參數名稱

import grails.web.RequestParameter

class AccountingController {

   // mainAccountNumber will be initialized with the value of params.accountNumber
   // accountType will be initialized with params.accountType
   def displayInvoice(@RequestParameter('accountNumber') String mainAccountNumber, int accountType) {
       // ...
   }
}

資料繫結與類型轉換錯誤

有時在執行資料繫結時,無法將特定字串轉換為特定目標類型。這會導致類型轉換錯誤。Grails 會保留 Grails 領域類別的 errors 屬性中的類型轉換錯誤。例如

class Book {
    ...
    URL publisherURL
}

在此我們有一個領域類別 Book,它使用 java.net.URL 類別來表示 URL。針對如下的輸入請求

/book/save?publisherURL=a-bad-url

無法將字串 a-bad-url 繫結到 publisherURL 屬性,因為會發生類型不符錯誤。您可以這樣檢查這些錯誤

def b = new Book(params)

if (b.hasErrors()) {
    println "The value ${b.errors.getFieldError('publisherURL').rejectedValue}" +
            " is not a valid URL!"
}

雖然我們尚未涵蓋錯誤碼(有關更多資訊,請參閱 驗證 區段),但對於類型轉換錯誤,您會希望使用 grails-app/i18n/messages.properties 檔案中的訊息作為錯誤。您可以使用一般錯誤訊息處理常式,例如

typeMismatch.java.net.URL=The field {0} is not a valid URL

或更具體的

typeMismatch.Book.publisherURL=The publisher URL you specified is not a valid URL

BindUsing 標註

BindUsing 標註可用於定義類別中特定欄位的自訂繫結機制。任何時候資料繫結套用到欄位時,標註的閉包值會以 2 個引數呼叫。第一個引數是資料繫結套用的物件,第二個引數是 DataBindingSource,也就是資料繫結的資料來源。從閉包傳回的值會繫結到屬性。以下範例會在資料繫結期間將來源中 name 值的大寫版本套用到 name 欄位。

import grails.databinding.BindUsing

class SomeClass {
    @BindUsing({obj, source ->

        //source is DataSourceBinding which is similar to a Map
        //and defines getAt operation but source.name cannot be used here.
        //In order to get name from source use getAt instead as shown below.

        source['name']?.toUpperCase()
    })
    String name
}
請注意,只有當請求參數的名稱與類別中的欄位名稱相符時,資料繫結才有可能。在此,請求參數中的 nameSomeClass 中的 name 相符。

BindUsing 標註可用於定義特定類別中所有欄位的自訂繫結機制。當標註套用到類別時,指定給標註的值應為實作 BindingHelper 介面的類別。當值繫結到已套用此標註的類別中的屬性時,會使用該類別的執行個體。

@BindUsing(SomeClassWhichImplementsBindingHelper)
class SomeClass {
    String someProperty
    Integer someOtherProperty
}

BindInitializer 標註

如果未定義,BindInitializer 標註可用於初始化類別中的關聯欄位。與 BindUsing 標註不同,資料繫結會繼續繫結此關聯中的所有巢狀屬性。

import grails.databinding.BindInitializer

class Account{}

class User {
  Account account

  // BindInitializer expects you to return a instance of the type
  // where it's declared on. You can use source as a parameter, in this case user.
  @BindInitializer({user-> new Contact(account:user.account) })
  Contact contact
}
class Contact{
  Account account
  String firstName
}
@BindInitializer 僅對關聯實體有意義,根據此用例。

自訂資料轉換器

繫結器會自動執行大量的類型轉換。有些應用程式可能想要定義自己的值轉換機制,而執行此項操作的簡單方法是撰寫實作 ValueConverter 的類別,並將該類別的執行個體註冊為 Spring 應用程式內容中的 bean。

package com.myapp.converters

import grails.databinding.converters.ValueConverter

/**
 * A custom converter which will convert String of the
 * form 'city:state' into an Address object.
 */
class AddressValueConverter implements ValueConverter {

    boolean canConvert(value) {
        value instanceof String
    }

    def convert(value) {
        def pieces = value.split(':')
        new com.myapp.Address(city: pieces[0], state: pieces[1])
    }

    Class<?> getTargetType() {
        com.myapp.Address
    }
}

該類別的執行個體需要註冊為 Spring 應用程式內容中的 bean。bean 名稱並不重要。所有實作 ValueConverter 的 bean 都會自動插入資料繫結程序。

grails-app/conf/spring/resources.groovy
beans = {
    addressConverter com.myapp.converters.AddressValueConverter
    // ...
}
class Person {
    String firstName
    Address homeAddress
}

class Address {
    String city
    String state
}

def person = new Person()
person.properties = [firstName: 'Jeff', homeAddress: "O'Fallon:Missouri"]
assert person.firstName == 'Jeff'
assert person.homeAddress.city = "O'Fallon"
assert person.homeAddress.state = 'Missouri'

資料繫結的日期格式

在將字串繫結至日期值時,可套用 BindingFormat 註解至日期欄位,以指定自訂日期格式。

import grails.databinding.BindingFormat

class Person {
    @BindingFormat('MMddyyyy')
    Date birthDate
}

可在 application.groovy 中設定全域設定,以定義繫結至日期時將在應用程式中使用的日期格式。

grails-app/conf/application.groovy
grails.databinding.dateFormats = ['MMddyyyy', 'yyyy-MM-dd HH:mm:ss.S', "yyyy-MM-dd'T'hh:mm:ss'Z'"]

grails.databinding.dateFormats 中指定的格式將按其在清單中包含的順序嘗試。如果屬性標記為 @BindingFormat,則 @BindingFormat 將優先於 grails.databinding.dateFormats 中指定的數值。

預設設定的格式為

  • yyyy-MM-dd HH:mm:ss.S

  • yyyy-MM-dd’T’hh:mm:ss’Z'

  • yyyy-MM-dd HH:mm:ss.S z

  • yyyy-MM-dd’T’HH:mm:ss.SSSX

自訂格式化轉換器

您可以透過撰寫實作 FormattedValueConverter 介面的類別,並將該類別的執行個體註冊為 Spring 應用程式內容中的 bean,來提供 BindingFormat 註解的處理常式。以下是簡單自訂字串格式化器的範例,它可能會根據指定給 BindingFormat 註解的值來轉換字串的大小寫。

package com.myapp.converters

import grails.databinding.converters.FormattedValueConverter

class FormattedStringValueConverter implements FormattedValueConverter {
    def convert(value, String format) {
        if('UPPERCASE' == format) {
            value = value.toUpperCase()
        } else if('LOWERCASE' == format) {
            value = value.toLowerCase()
        }
        value
    }

    Class getTargetType() {
        // specifies the type to which this converter may be applied
        String
    }
}

該類別的執行個體需要註冊為 Spring 應用程式內容中的 bean。bean 名稱並不重要。所有實作 FormattedValueConverter 的 bean 都會自動插入資料繫結程序中。

grails-app/conf/spring/resources.groovy
beans = {
    formattedStringConverter com.myapp.converters.FormattedStringValueConverter
    // ...
}

這樣一來,BindingFormat 註解就可以套用至字串欄位,以告知資料繫結器利用自訂轉換器。

import grails.databinding.BindingFormat

class Person {
    @BindingFormat('UPPERCASE')
    String someUpperCaseString

    @BindingFormat('LOWERCASE')
    String someLowerCaseString

    String someOtherString
}

在地化繫結格式

BindingFormat 註解透過使用選用的 code 屬性來支援在地化格式字串。如果將值指定給 code 屬性,則該值將用作訊息代碼,以從 Spring 應用程式內容中的 messageSource bean 擷取繫結格式字串,而該查詢將會在地化。

import grails.databinding.BindingFormat

class Person {
    @BindingFormat(code='date.formats.birthdays')
    Date birthDate
}
# grails-app/conf/i18n/messages.properties
date.formats.birthdays=MMddyyyy
# grails-app/conf/i18n/messages_es.properties
date.formats.birthdays=ddMMyyyy

結構化資料繫結編輯器

結構化資料繫結編輯器是一種輔助類別,它可以將結構化要求參數繫結至屬性。結構化繫結的常見使用案例是繫結至 Date 物件,該物件可能會由幾個較小的資訊片段建構而成,這些片段包含在幾個要求參數中,其名稱類似於 birthday_monthbirthday_datebirthday_year。結構化編輯器將擷取所有這些個別資訊片段,並使用它們來建構 Date

此架構提供一個結構化編輯器,用於繫結至 Date 物件。應用程式可以註冊它自己的結構化編輯器,以適用於任何適當的類型。請考慮下列類別

src/main/groovy/databinding/Gadget.groovy
package databinding

class Gadget {
    Shape expandedShape
    Shape compressedShape
}
src/main/groovy/databinding/Shape.groovy
package databinding

class Shape {
    int area
}

Gadget 有 2 個 Shape 欄位。Shapearea 屬性。應用程式可能想要接受 widthheight 等要求參數,並在繫結時間使用這些參數來計算 Shapearea。結構化繫結編輯器非常適合於此。

將結構化編輯器註冊到資料繫結程序的方法是將 grails.databinding.TypedStructuredBindingEditor 介面的實例新增到 Spring 應用程式內容。實作 TypedStructuredBindingEditor 介面最簡單的方法是延伸 org.grails.databinding.converters.AbstractStructuredBindingEditor 抽象類別,並覆寫 getPropertyValue 方法,如下所示

src/main/groovy/databinding/converters/StructuredShapeEditor.groovy
package databinding.converters

import databinding.Shape

import org.grails.databinding.converters.AbstractStructuredBindingEditor

class StructuredShapeEditor extends AbstractStructuredBindingEditor<Shape> {

    public Shape getPropertyValue(Map values) {
        // retrieve the individual values from the Map
        def width = values.width as int
        def height = values.height as int

        // use the values to calculate the area of the Shape
        def area = width * height

        // create and return a Shape with the appropriate area
        new Shape(area: area)
    }
}

該類別的實例需要註冊到 Spring 應用程式內容

grails-app/conf/spring/resources.groovy
beans = {
    shapeEditor databinding.converters.StructuredShapeEditor
    // ...
}

當資料繫結器繫結到 Gadget 類別的實例時,它會檢查是否有名稱為 compressedShapeexpandedShape 的要求參數,其值為「struct」,如果存在,將觸發使用 StructuredShapeEditor。結構的個別元件需要有 propertyName_structuredElementName 形式的參數名稱。在上述 Gadget 類別的情況下,這表示 compressedShape 要求參數的值應該是「struct」,而 compressedShape_widthcompressedShape_height 參數應該有代表壓縮 Shape 的寬度和高度的值。類似地,expandedShape 要求參數的值應該是「struct」,而 expandedShape_widthexpandedShape_height 參數應該有代表展開 Shape 的寬度和高度的值。

grails-app/controllers/demo/DemoController.groovy
class DemoController {

    def createGadget(Gadget gadget) {
        /*
        /demo/createGadget?expandedShape=struct&expandedShape_width=80&expandedShape_height=30
                          &compressedShape=struct&compressedShape_width=10&compressedShape_height=3

        */

        // with the request parameters shown above gadget.expandedShape.area would be 2400
        // and gadget.compressedShape.area would be 30
        // ...
    }
}

通常,值為「struct」的要求參數會由隱藏的表單欄位表示。

資料繫結事件監聽器

DataBindingListener 介面提供了一種機制,讓監聽器可以接收資料繫結事件的通知。介面如下所示

package grails.databinding.events;

import grails.databinding.errors.BindingError;

/**
 * A listener which will be notified of events generated during data binding.
 *
 * @author Jeff Brown
 * @since 3.0
 * @see DataBindingListenerAdapter
 */
public interface DataBindingListener {

    /**
     * @return true if the listener is interested in events for the specified type.
     */
    boolean supports(Class<?> clazz);

    /**
     * Called when data binding is about to start.
     *
     * @param target The object data binding is being imposed upon
     * @param errors the Spring Errors instance (a org.springframework.validation.BindingResult)
     * @return true if data binding should continue
     */
    Boolean beforeBinding(Object target, Object errors);

    /**
     * Called when data binding is about to imposed on a property
     *
     * @param target The object data binding is being imposed upon
     * @param propertyName The name of the property being bound to
     * @param value The value of the property being bound
     * @param errors the Spring Errors instance (a org.springframework.validation.BindingResult)
     * @return true if data binding should continue, otherwise return false
     */
    Boolean beforeBinding(Object target, String propertyName, Object value, Object errors);

    /**
     * Called after data binding has been imposed on a property
     *
     * @param target The object data binding is being imposed upon
     * @param propertyName The name of the property that was bound to
     * @param errors the Spring Errors instance (a org.springframework.validation.BindingResult)
     */
    void afterBinding(Object target, String propertyName, Object errors);

    /**
     * Called after data binding has finished.
     *
     * @param target The object data binding is being imposed upon
     * @param errors the Spring Errors instance (a org.springframework.validation.BindingResult)
     */
    void afterBinding(Object target, Object errors);

    /**
     * Called when an error occurs binding to a property
     * @param error encapsulates information about the binding error
     * @param errors the Spring Errors instance (a org.springframework.validation.BindingResult)
     * @see BindingError
     */
    void bindingError(BindingError error, Object errors);
}

實作該介面的 Spring 應用程式內容中的任何 bean 都會自動註冊到資料繫結器。 DataBindingListenerAdapter 類別實作 DataBindingListener 介面,並提供介面中所有方法的預設實作,因此此類別非常適合用於子類別化,因此您的監聽器類別只需要提供監聽器感興趣的方法實作即可。

直接使用資料繫結器

在某些情況下,應用程式可能想要直接使用資料繫結器。例如,在服務中對非網域類別的任意物件進行繫結。以下程式碼無法執行,因為 properties 屬性為唯讀。

src/main/groovy/bindingdemo/Widget.groovy
package bindingdemo

class Widget {
    String name
    Integer size
}
grails-app/services/bindingdemo/WidgetService.groovy
package bindingdemo

class WidgetService {

    def updateWidget(Widget widget, Map data) {
        // this will throw an exception because
        // properties is read-only
        widget.properties = data
    }
}

資料繫結器的執行個體位於 Spring 應用程式內容中,bean 名稱為 grailsWebDataBinder。該 bean 實作 DataBinder 介面。以下程式碼示範如何直接使用資料繫結器。

grails-app/services/bindingdmeo/WidgetService
package bindingdemo

import grails.databinding.SimpleMapDataBindingSource

class WidgetService {

    // this bean will be autowired into the service
    def grailsWebDataBinder

    def updateWidget(Widget widget, Map data) {
        grailsWebDataBinder.bind widget, data as SimpleMapDataBindingSource
    }

}

請參閱 DataBinder 文件,以取得有關 bind 方法重載版本的更多資訊。

資料繫結和安全性疑慮

從請求參數批次更新屬性時,您需要小心,不要讓客戶端將惡意資料繫結到網域類別中,並儲存在資料庫中。您可以使用下標運算子限制繫結到特定網域類別的屬性

def p = Person.get(1)

p.properties['firstName','lastName'] = params

在這種情況下,只會繫結 firstNamelastName 屬性。

另一種方法是使用 命令物件 作為資料繫結的目標,而不是網域類別。或者,也可以使用彈性的 bindData 方法。

bindData 方法允許相同的資料繫結功能,但針對任意物件

def p = new Person()
bindData(p, params)

bindData 方法也讓您可以排除您不想要更新的特定參數

def p = new Person()
bindData(p, params, [exclude: 'dateOfBirth'])

或只包含特定屬性

def p = new Person()
bindData(p, params, [include: ['firstName', 'lastName']])
如果提供一個空的清單作為 include 參數的值,則所有欄位都將受到繫結,除非它們被明確排除。

bindable 約束可用於全面禁止某些屬性的資料繫結。

7.1.6 以 JSON 回應

使用 respond 方法輸出 JSON

respond 方法是傳回 JSON 的首選方式,並與 內容協商JSON 檢視 整合。

respond 方法提供內容協商策略,以智慧地產生適合特定客戶端的適當回應。

例如,給定以下控制器和動作

grails-app/controllers/example/BookController.groovy
package example

class BookController {
    def index() {
        respond Book.list()
    }
}

respond 方法將執行下列步驟

  1. 如果客戶端 Accept 標頭指定媒體類型(例如 application/json),則使用該標頭

  2. 如果 URI 的檔案副檔名(例如 /books.json)包含 grails-app/conf/application.ymlgrails.mime.types 屬性中定義的格式,則使用設定中定義的媒體類型

respond 方法將會尋找一個合適的 Renderer 給物件和從 RendererRegistry 計算出的媒體類型。

Grails 包含許多預先設定的 Renderer 實作,它們會產生傳遞給 respond 的參數的 JSON 回應的預設表示。例如,前往 /book.json URI 會產生以下的 JSON

[
    {id:1,"title":"The Stand"},
    {id:2,"title":"Shining"}
]

控制媒體類型的優先順序

預設情況下,如果您定義一個控制器,則在傳送回客戶端的格式方面沒有優先順序,而 Grails 會假設您希望將 HTML 提供為回應類型。

但是,如果您的應用程式主要是 API,則可以使用 responseFormats 屬性指定優先順序

grails-app/controllers/example/BookController.groovy
package example

class BookController {
    static responseFormats = ['json', 'html']
    def index() {
        respond Book.list()
    }
}

在上述範例中,如果無法從 Accept 標頭或檔案副檔名計算出回應的媒體類型,Grails 會預設以 json 回應。

使用檢視來輸出 JSON 回應

如果您定義一個檢視(GSP 或 JSON 檢視),則 Grails 會在使用 respond 方法時透過從傳遞給 respond 的參數計算一個模型來呈現檢視。

例如,在先前的清單中,如果您要定義 grails-app/views/index.gsongrails-app/views/index.gsp 檢視,則當客戶端分別要求 application/jsontext/html 媒體類型時,將會使用這些檢視。因此,您可以定義一個後端,能夠對網頁瀏覽器提供回應或表示應用程式的 API。

在呈現檢視時,Grails 會計算一個模型傳遞給檢視,該模型基於傳遞給 respond 方法的值的類型。

下表總結了這個慣例

範例 參數類型 計算出的模型變數

respond Book.list()

java.util.List

bookList

respond( [] )

java.util.List

emptyList

respond Book.get(1)

example.Book

book

respond( [1,2] )

java.util.List

integerList

respond( [1,2] as Set )

java.util.Set

integerSet

respond( [1,2] as Integer[] )

Integer[]

integerArray

使用此慣例,您可以在檢視中參照傳遞給 respond 的引數

grails-app/views/book/index.gson
@Field List<Book> bookList = []

json bookList, { Book book ->
    title book.title
}

您會注意到,如果 Book.list() 傳回一個空清單,則模型變數名稱會轉換為 emptyList。這是設計使然,如果您未指定模型變數,則應在檢視中提供預設值,例如上述範例中的 List

grails-app/views/book/index.gson
// defaults to an empty list
@Field List<Book> bookList = []
...

在某些情況下,您可能希望更明確並控制模型變數的名稱。例如,如果您有網域繼承階層,其中對 list() 的呼叫可能會傳回不同的子類別,則依賴自動計算可能不可靠。

在這種情況下,您應該使用 respond 和一個映射引數直接傳遞模型

respond bookList: Book.list()
在集合中回應任何類型的混合引數時,請務必使用明確的模型名稱。

如果您只是想擴充計算出的模型,則可以透過傳遞 model 引數來執行此操作

respond Book.list(), [model: [bookCount: Book.count()]]

上述範例會產生一個類似 [bookList:books, bookCount:totalBooks] 的模型,其中計算出的模型與 model 引數中傳遞的模型合併。

使用 render 方法輸出 JSON

render 方法也可以用於輸出 JSON,但僅應使用於不需要建立 JSON 檢視的簡單情況

def list() {

    def results = Book.list()

    render(contentType: "application/json") {
        books(results) { Book b ->
            title b.title
        }
    }
}

在這種情況下,結果將類似於

[
    {"title":"The Stand"},
    {"title":"Shining"}
]
這種用於呈現 JSON 的技術對於非常簡單的回應來說可能沒問題,但一般來說,您應該優先使用 JSON 檢視,並使用檢視層,而不是在應用程式中嵌入邏輯。

上面針對 XML 描述的命名衝突的危險也適用於 JSON 建立。

7.1.7 更多關於 JSONBuilder

前面關於 XML 和 JSON 回應的章節涵蓋了呈現 XML 和 JSON 回應的簡化範例。Grails 使用的 XML 建構器是 Groovy 中的標準 XmlSlurper

對於 JSON,自 Grails 3.1 起,Grails 預設使用 Groovy 的 StreamingJsonBuilder,你可以參考 Groovy 文件StreamingJsonBuilder API 文件了解如何使用它。

7.1.8 以 XML 回應

7.1.9 上傳檔案

程式化檔案上傳

Grails 支援使用 Spring 的 MultipartHttpServletRequest 介面進行檔案上傳。檔案上傳的第一步是建立一個 multipart 表單,如下所示

Upload Form: <br />
    <g:uploadForm action="upload">
        <input type="file" name="myFile" />
        <input type="submit" />
    </g:uploadForm>

uploadForm 標籤會方便地將 enctype="multipart/form-data" 屬性新增到標準的 <g:form> 標籤。

接著有許多方法可以處理檔案上傳。其中一種方法是直接使用 Spring 的 MultipartFile 執行個體

def upload() {
    def f = request.getFile('myFile')
    if (f.empty) {
        flash.message = 'file cannot be empty'
        render(view: 'uploadForm')
        return
    }

    f.transferTo(new File('/some/local/dir/myfile.txt'))
    response.sendError(200, 'Done')
}

這對於傳輸到其他目的地和直接操作檔案很方便,因為你可以使用 MultipartFile 介面取得 InputStream 等。

透過資料繫結進行檔案上傳

檔案上傳也可以使用資料繫結來執行。考慮這個 Image 領域類別

class Image {
    byte[] myFile

    static constraints = {
        // Limit upload file size to 2MB
        myFile maxSize: 1024 * 1024 * 2
    }
}

如果你在建構函式中使用 params 物件建立影像,如下面的範例所示,Grails 會自動將檔案內容繫結為 byte[]myFile 屬性

def img = new Image(params)

設定 sizemaxSize 約束很重要,否則你的資料庫可能會建立一個小欄位大小,無法處理合理大小的檔案。例如,H2 和 MySQL 預設將 byte[] 屬性的 blob 大小設為 255 位元組。

也可以透過將影像中 myFile 屬性的類型變更為字串類型,將檔案內容設定為字串

class Image {
   String myFile
}

增加上傳檔案最大值

Grails 上傳檔案的預設大小為 128000 (~128KB)。超過此限制時,你會看到以下例外

org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException

您可以在 application.yml 中設定限制,如下所示

grails-app/conf/application.yml
grails:
    controllers:
        upload:
            maxFileSize: 2000000
            maxRequestSize: 2000000

maxFileSize = 上傳檔案允許的最大大小。

maxRequestSize = multipart/form-data 要求允許的最大大小。

將檔案大小限制在最大值以防止拒絕服務攻擊。

這些限制存在於防止 DoS 攻擊和強制整體應用程式效能

7.1.10 命令物件

Grails 控制器支援命令物件的概念。命令物件是一個與 資料繫結 結合使用的類別,通常允許驗證可能不符合現有網域類別的資料。

只有在將類別用作動作參數時,才會將其視為命令物件。

宣告命令物件

命令物件類別的定義與其他類別相同。

class LoginCommand implements grails.validation.Validateable {
    String username
    String password

    static constraints = {
        username(blank: false, minSize: 6)
        password(blank: false, minSize: 6)
    }
}

在此範例中,命令物件類別實作 Validateable 特質。Validateable 特質允許定義 約束,就像在 網域類別 中一樣。如果命令物件定義在與使用它的控制器相同的原始檔中,Grails 會自動使其成為 Validateable。命令物件類別不需要是可驗證的。

預設情況下,所有 Validateable 物件屬性(不是 java.util.Collectionjava.util.Map 的實例)都是 nullable: falsejava.util.Collectionjava.util.Map 的實例預設為 nullable: true。如果您想要一個預設為 nullable: true 屬性的 Validateable,您可以透過在類別中定義 defaultNullable 方法來指定

class AuthorSearchCommand implements grails.validation.Validateable {
    String  name
    Integer age

    static boolean defaultNullable() {
        true
    }
}

在此範例中,nameage 都會在驗證期間允許空值。

使用命令物件

若要使用命令物件,控制器動作可以選擇性地指定任意數量的命令物件參數。必須提供參數類型,以便 Grails 知道要建立和初始化哪些物件。

在執行控制器動作之前,Grails 會自動建立命令物件類別的實例,並透過繫結要求參數來填入其屬性。如果命令物件類別標記為 Validateable,則會驗證命令物件。例如

class LoginController {

    def login(LoginCommand cmd) {
        if (cmd.hasErrors()) {
            redirect(action: 'loginForm')
            return
        }

        // work with the command object data
    }
}

如果命令物件的類型是網域類別,且有一個 id 要求參數,則會呼叫網域類別上的靜態 get 方法,並將 id 參數的值傳遞為引數,而不是呼叫網域類別建構函數來建立新實例。

從呼叫 get 傳回的任何內容都會傳遞到控制器動作中。這表示如果有一個 id 要求參數,而且在資料庫中找不到對應的記錄,則命令物件的值會是 null。如果從資料庫中擷取執行個體時發生錯誤,則會將 null 傳遞為控制器動作的引數,而且會將錯誤新增到控制器的 errors 屬性。

如果命令物件的類型是網域類別,而且沒有 id 要求參數,或是有 id 要求參數但其值為空,則會將 null 傳遞到控制器動作中,除非 HTTP 要求方法為「POST」,這種情況下會透過呼叫網域類別建構函式來建立網域類別的新執行個體。對於網域類別執行個體為非 null 的所有情況,只有在 HTTP 要求方法為「POST」、「PUT」或「PATCH」時才會執行資料繫結。

命令物件和要求參數名稱

通常要求參數名稱會直接對應到命令物件中的屬性名稱。巢狀參數名稱可以用來以直覺的方式繫結物件圖形。

在以下範例中,名為 name 的要求參數會繫結到 Person 執行個體的 name 屬性,而名為 address.city 的要求參數會繫結到 Personaddress 屬性的 city 屬性。

class StoreController {
    def buy(Person buyer) {
        // ...
    }
}

class Person {
    String name
    Address address
}

class Address {
    String city
}

如果控制器動作接受多個命令物件,而這些物件碰巧包含相同屬性名稱,可能會出現問題。請考慮以下範例。

class StoreController {
    def buy(Person buyer, Product product) {
        // ...
    }
}

class Person {
    String name
    Address address
}

class Address {
    String city
}

class Product {
    String name
}

如果有一個名為 name 的要求參數,則不清楚這應表示 Product 的名稱還是 Person 的名稱。如果控制器動作接受 2 個相同類型的命令物件,則可能會出現問題的另一個版本,如下所示。

class StoreController {
    def buy(Person buyer, Person seller, Product product) {
        // ...
    }
}

class Person {
    String name
    Address address
}

class Address {
    String city
}

class Product {
    String name
}

為了協助處理這個問題,架構會強制執行特殊規則,以對應參數名稱到命令物件類型。命令物件資料繫結會將所有以控制器動作參數名稱開頭的參數視為屬於對應的命令物件。

例如,product.name 要求參數會繫結到 product 引數中的 name 屬性,buyer.name 要求參數會繫結到 buyer 引數中的 name 屬性,seller.address.city 要求參數會繫結到 seller 引數中 address 屬性的 city 屬性,等等…​

命令物件和相依性注入

命令物件可以參與依賴注入。如果您的命令物件具有使用 Grails 服務 的自訂驗證邏輯,這會很有用

class LoginCommand implements grails.validation.Validateable {

    def loginService

    String username
    String password

    static constraints = {
        username validator: { val, obj ->
            obj.loginService.canLogin(obj.username, obj.password)
        }
    }
}

在此範例中,命令物件會與由 Spring ApplicationContext 根據名稱注入的 loginService bean 互動。

將要求主體繫結到命令物件

當對接受命令物件的控制器動作提出要求,且要求包含主體時,Grails 會根據要求內容類型解析要求的主體,並使用主體對命令物件執行資料繫結。請參閱下列範例。

grails-app/controllers/bindingdemo/DemoController.groovy
package bindingdemo

class DemoController {

    def createWidget(Widget w) {
        render "Name: ${w?.name}, Size: ${w?.size}"
    }
}

class Widget {
    String name
    Integer size
}
$ curl -H "Content-Type: application/json" -d '{"name":"Some Widget","42"}'[size] localhost:8080/demo/createWidget
 Name: Some Widget, Size: 42

$ curl -H "Content-Type: application/xml" -d '<widget><name>Some Other Widget</name><size>2112</size></widget>' localhost:8080/bodybind/demo/createWidget
 Name: Some Other Widget, Size: 2112

在下列情況下,不會解析要求主體

  • 要求方法為 GET

  • 要求方法為 DELETE

  • 內容長度為 0

請注意,正在解析要求主體以使其運作。之後,任何嘗試讀取要求主體的行為都會失敗,因為對應的輸入串流會是空的。控制器動作可以使用命令物件,或自行解析要求主體(直接解析或參照類似 request.JSON 的內容),但無法同時執行這兩個動作。

grails-app/controllers/bindingdemo/DemoController.groovy
package bindingdemo

class DemoController {

    def createWidget(Widget w) {
        // this will fail because it requires reading the body,
        // which has already been read.
        def json = request.JSON

        // ...

    }
}

使用命令物件清單

命令物件的常見使用案例是包含另一個集合的命令物件

class DemoController {

    def createAuthor(AuthorCommand command) {
        // ...

    }

    class AuthorCommand {
        String fullName
        List<BookCommand> books
    }

    class BookCommand {
        String title
        String isbn
    }
}

在此範例中,我們要建立一個包含多本書籍的作者。

若要從 UI 層執行此作業,您可以在 GSP 中執行下列動作

<g:form name="submit-author-books" controller="demo" action="createAuthor">
    <g:fieldValue name="fullName" value=""/>
    <ul>
        <li>
            <g:fieldValue name="books[0].title" value=""/>
            <g:fieldValue name="books[0].isbn" value=""/>
        </li>

        <li>
            <g:fieldValue name="books[1].title" value=""/>
            <g:fieldValue name="books[1].isbn" value=""/>
        </li>
    </ul>
</g:form>

也支援 JSON,因此您可以提交下列內容以進行正確的資料繫結

{
    "fullName": "Graeme Rocher",
    "books": [{
        "title": "The Definitive Guide to Grails",
        "isbn": "1111-343455-1111"
    }, {
        "title": "The Definitive Guide to Grails 2",
        "isbn": "1111-343455-1112"
    }],
}

7.1.11 處理重複的表單提交

Grails 內建支援使用「同步化代幣模式」處理重複的表單提交。若要開始,您可以在 表單 標籤上定義代幣

<g:form useToken="true" ...>

然後,您可以在控制器程式碼中使用 withForm 方法來處理有效的和無效的要求

withForm {
   // good request
}.invalidToken {
   // bad request
}

如果您只提供 withForm 方法,而不提供鏈結的 invalidToken 方法,則預設 Grails 會將無效代幣儲存在 flash.invalidToken 變數中,並將要求重新導向回原始頁面。然後,可以在檢視中檢查此內容

<g:if test="${flash.invalidToken}">
  Don't click the button twice!
</g:if>
withForm 標籤使用 session,因此如果在叢集中使用,則需要會話關聯性或叢集會話。

7.1.12 簡單類型轉換器

類型轉換方法

如果您偏好避免資料繫結的開銷,而僅想將輸入參數(通常為字串)轉換成另一種更適當的類型,params物件對每種類型都有許多便利的方法

def total = params.int('total')

上述範例使用int方法,還有方法可供booleanlongcharshort等使用。這些方法都具有安全性,不會發生任何剖析錯誤,因此您不必對參數執行任何額外檢查。

每種轉換方法都允許將預設值傳遞為第二個選用引數。如果在對應項目中找不到對應的項目,或者轉換期間發生錯誤,將會傳回預設值。範例

def total = params.int('total', 42)

這些相同的類型轉換方法也可在 GSP 標籤的attrs參數中使用。

處理多個參數

常見的用例是處理多個同名的請求參數。例如,您可以取得類似於?name=Bob&name=Judy的查詢字串。

在此情況下,處理一個參數和處理多個參數具有不同的語意,因為 Groovy 的String反覆運算機制會反覆運算每個字元。為避免此問題,params物件提供一個總是傳回清單的list方法

for (name in params.list('name')) {
    println name
}

7.1.13 宣告式控制器例外處理

Grails 控制器支援宣告式例外處理的簡單機制。如果控制器宣告一個接受單一引數的方法,而且引數類型為java.lang.Exceptionjava.lang.Exception的某個子類別,則當該控制器中的動作擲出該類型的例外時,將會呼叫該方法。請參閱下列範例。

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

class DemoController {

    def someAction() {
        // do some work
    }

    def handleSQLException(SQLException e) {
        render 'A SQLException Was Handled'
    }

    def handleBatchUpdateException(BatchUpdateException e) {
        redirect controller: 'logging', action: 'batchProblem'
    }

    def handleNumberFormatException(NumberFormatException nfe) {
        [problemDescription: 'A Number Was Invalid']
    }
}

該控制器將會表現得好像是以類似於下列方式撰寫的…​

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

class DemoController {

    def someAction() {
        try {
            // do some work
        } catch (BatchUpdateException e) {
            return handleBatchUpdateException(e)
        } catch (SQLException e) {
            return handleSQLException(e)
        } catch (NumberFormatException e) {
            return handleNumberFormatException(e)
        }
    }

    def handleSQLException(SQLException e) {
        render 'A SQLException Was Handled'
    }

    def handleBatchUpdateException(BatchUpdateException e) {
        redirect controller: 'logging', action: 'batchProblem'
    }

    def handleNumberFormatException(NumberFormatException nfe) {
        [problemDescription: 'A Number Was Invalid']
    }
}

例外處理方法名稱可以是任何有效的函式名稱。名稱並非使方法成為例外處理方法的因素,Exception引數類型才是重要的部分。

例外處理方法可以執行控制器動作可以執行的任何動作,包括呼叫renderredirect、傳回模型等。

一種在多個控制器中共用例外處理方法的方式是使用繼承。例外處理方法會繼承到子類別中,因此應用程式可以在多個控制器延伸的抽象類別中定義例外處理方法。另一種在多個控制器中共用例外處理方法的方式是使用特質,如下所示…​

src/main/groovy/com/demo/DatabaseExceptionHandler.groovy
package com.demo

trait DatabaseExceptionHandler {
    def handleSQLException(SQLException e) {
        // handle SQLException
    }

    def handleBatchUpdateException(BatchUpdateException e) {
        // handle BatchUpdateException
    }
}
grails-app/controllers/com/demo/DemoController.groovy
package com.demo

class DemoController implements DatabaseExceptionHandler {

    // all of the exception handler methods defined
    // in DatabaseExceptionHandler will be added to
    // this class at compile time
}

例外處理器方法必須在編譯時存在。特別是,在執行時期元程式設計到控制器類別上的例外處理器方法不受支援。

7.2 Groovy Server Pages

Groovy Servers Pages (簡稱 GSP) 是 Grails 的檢視技術。它設計為讓 ASP 和 JSP 等技術的使用者感到熟悉,但更具彈性和直覺性。

儘管 GSP 可以呈現任何格式,不只 HTML,但它更專注於呈現標記。如果您正在尋找簡化 JSON 回應的方法,請參閱 JSON Views

GSP 位於 grails-app/views 目錄中,通常會自動呈現(依慣例)或使用 render 方法,例如

render(view: "index")

GSP 通常是標記和 GSP 標籤的組合,有助於檢視呈現。

儘管可以在 GSP 中嵌入 Groovy 邏輯,而且本文檔將涵蓋如何執行此操作,但強烈建議不要這樣做。混合標記和程式碼是一件糟糕的事,而且大多數 GSP 頁面都不包含程式碼,也不需要這樣做。

GSP 通常有一個「模型」,它是一組用於檢視呈現的變數。模型從控制器傳遞到 GSP 檢視。例如,考慮以下控制器動作

def show() {
    [book: Book.get(params.id)]
}

此動作將查詢一個 Book 實例,並建立一個包含稱為 book 的金鑰的模型。然後可以在 GSP 檢視中使用名稱 book 參照此金鑰

${book.title}
嵌入從使用者輸入接收的資料有讓您的應用程式容易受到跨網站指令碼 (XSS) 攻擊的風險。請閱讀 XSS 預防 文件,以了解如何預防 XSS 攻擊。

有關使用 GSP 的更多資訊,請參閱 專用的 GSP 文件

7.3 URL 對應

到目前為止,文件所使用的 URL 慣例是預設的 /controller/action/id。但是,此慣例並未硬性編寫到 Grails 中,實際上是由位於 grails-app/controllers/mypackage/UrlMappings.groovy 的 URL 對應類別控制。

UrlMappings 類別包含一個稱為 mappings 的單一屬性,已指定給一個程式碼區塊

package mypackage

class UrlMappings {
    static mappings = {
    }
}

7.3.1 對應到控制器和動作

若要建立一個簡單對應,只需使用相對 URL 作為方法名稱,並指定要對應到的控制器和動作的名稱參數

"/product"(controller: "product", action: "list")

在這個案例中,我們將 URL /product 對應到 ProductControllerlist 動作。省略動作定義以對應到控制器的預設動作

"/product"(controller: "product")

另一種語法是將要使用的控制器和動作指定在傳遞給方法的區塊中

"/product" {
    controller = "product"
    action = "list"
}

您使用的語法在很大程度上取決於個人偏好。

如果您有映射都屬於特定路徑,您可以使用 group 方法對映射進行分組

group "/product", {
    "/apple"(controller:"product", id:"apple")
    "/htc"(controller:"product", id:"htc")
}

您還可以建立巢狀 group url 映射

group "/store", {
    group "/product", {
        "/$id"(controller:"product")
    }
}

若要將一個 URI 重寫到另一個明確的 URI(而不是控制器/動作配對),請執行類似以下操作

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

在與其他架構整合時,重新寫入特定 URI 通常很有用。

7.3.2 映射到 REST 資源

自 Grails 2.3 以來,可以建立 RESTful URL 映射,根據慣例映射到控制器。這樣做的語法如下

"/books"(resources:'book')

您定義基本 URI 和要使用 resources 參數映射到的控制器名稱。上述映射將產生以下 URL

HTTP 方法 URI Grails 動作

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

如果您不確定為您的案例產生哪個映射,請在您的 grails 主控台中執行命令 url-mappings-report。它將為您提供所有 url 映射的整潔報告。

如果您希望包含或排除任何產生的 URL 映射,您可以使用 includesexcludes 參數,它接受要包含或排除的 Grails 動作名稱

"/books"(resources:'book', excludes:['delete', 'update'])

or

"/books"(resources:'book', includes:['index', 'show'])

明確的 REST 映射

自 Grails 3.1 起,如果您不希望依賴 resources 映射來定義您的映射,那麼您可以使用 HTTP 方法名稱(小寫)為任何 URL 映射加上前綴,以表示它適用的 HTTP 方法。下列 URL 映射

"/books"(resources:'book')

等於

get "/books"(controller:"book", action:"index")
get "/books/create"(controller:"book", action:"create")
post "/books"(controller:"book", action:"save")
get "/books/$id"(controller:"book", action:"show")
get "/books/$id/edit"(controller:"book", action:"edit")
put "/books/$id"(controller:"book", action:"update")
delete "/books/$id"(controller:"book", action:"delete")

請注意 HTTP 方法名稱在每個 URL 映射定義之前加上前綴。

單一資源

單一資源是系統中只有一個(可能每位使用者一個)的資源。您可以使用 single 參數(與 resources 相反)建立單一資源

"/book"(single:'book')

這會產生以下 URL 映射

HTTP 方法 URI Grails 動作

GET

/book/create

create

POST

/book

save

GET

/book

show

GET

/book/edit

edit

PUT

/book

update

DELETE

/book

delete

主要的差異是 id 未包含在 URL 映射中。

巢狀資源

您可以巢狀資源映射以產生子資源。例如

"/books"(resources:'book') {
  "/authors"(resources:"author")
}

上述將產生以下 URL 映射

HTTP 方法 URL Grails 動作

GET

/books/${bookId}/authors

index

GET

/books/${bookId}/authors/create

create

POST

/books/${bookId}/authors

save

GET

/books/${bookId}/authors/${id}

show

GET

/books/${bookId}/authors/edit/${id}

edit

PUT

/books/${bookId}/authors/${id}

update

DELETE

/books/${bookId}/authors/${id}

delete

您還可以在資源映射中巢狀常規 URL 映射

"/books"(resources: "book") {
    "/publisher"(controller:"publisher")
}

這將導致下列 URL 可用

HTTP 方法 URL Grails 動作

GET

/books/${bookId}/publisher

index

若要將 URI 直接對應到資源下方,請使用集合區塊

"/books"(resources: "book") {
    collection {
        "/publisher"(controller:"publisher")
    }
}

這將導致下列 URL 可用(不含 ID)

HTTP 方法 URL Grails 動作

GET

/books/publisher

index

連結到 RESTful 對應

您可以連結到任何使用 Grails 提供的 g:link 標籤建立的 URL 對應,只要參考要連結的控制器和動作即可

<g:link controller="book" action="index">My Link</g:link>

為了方便起見,您也可以將網域實例傳遞給 link 標籤的 resource 屬性

<g:link resource="${book}">My Link</g:link>

這將自動產生正確的連結(在本例中,ID 為「1」時為「/books/1」)。

巢狀資源的情況略有不同,因為它們通常需要兩個識別碼(資源的 ID 和它所巢狀的資源的 ID)。例如,假設有巢狀資源

"/books"(resources:'book') {
  "/authors"(resources:"author")
}

如果您希望連結到 author 控制器的 show 動作,您可以寫入

// Results in /books/1/authors/2
<g:link controller="author" action="show" method="GET" params="[bookId:1]" id="2">The Author</g:link>

但是,為了讓這更簡潔,連結標籤有一個 resource 屬性,可以用來代替

// Results in /books/1/authors/2
<g:link resource="book/author" action="show" bookId="1" id="2">My Link</g:link>

資源屬性接受以斜線分隔的資源路徑(在本例中為「book/author」)。標籤的屬性可用於指定必要的 bookId 參數。

7.3.3 URL 對應中的重新導向

從 Grails 2.3 開始,可以定義指定重新導向的 URL 對應。當 URL 對應指定重新導向時,任何時候該對應符合傳入請求,就會啟動重新導向,並提供對應提供的資訊。

當 URL 對應指定重新導向時,對應必須提供表示要重新導向到的 URI 的字串,或必須提供表示重新導向目標的 Map。該 Map 的結構就像可能傳遞給控制器中 redirect 方法的 Map。

"/viewBooks"(redirect: [uri: '/books/list'])
"/viewAuthors"(redirect: [controller: 'author', action: 'list'])
"/viewPublishers"(redirect: [controller: 'publisher', action: 'list', permanent: true])

預設情況下,原始請求中包含的請求參數不會包含在重新導向中。若要包含它們,必須新增參數 keepParamsWhenRedirect: true

"/viewBooks"(redirect: [uri: '/books/list', keepParamsWhenRedirect: true])
"/viewAuthors"(redirect: [controller: 'author', action: 'list', keepParamsWhenRedirect: true])
"/viewPublishers"(redirect: [controller: 'publisher', action: 'list', permanent: true, keepParamsWhenRedirect: true])

7.3.4 內嵌變數

簡單變數

前一節說明如何使用具體「代碼」對應簡單 URL。在 URL 對應中,代碼是每個斜線「/」之間的字元序列。具體代碼是一個定義良好的代碼,例如 /product。但是,在許多情況下,您不知道特定代碼的值,直到執行時。在這種情況下,您可以在 URL 中使用變數佔位符,例如

static mappings = {
  "/product/$id"(controller: "product")
}

在本例中,透過將 $id 變數內嵌為第二個代碼,Grails 會自動將第二個代碼對應到一個參數(可透過 params 物件取得),稱為 id。例如,給定 URL /product/MacBook,下列程式碼會將「MacBook」呈現到回應中

class ProductController {
     def index() { render params.id }
}

當然,您可以建構更複雜的對應範例。例如,傳統部落格 URL 格式可以對應如下

static mappings = {
   "/$blog/$year/$month/$day/$id"(controller: "blog", action: "show")
}

上述對應讓您可以執行下列動作

/graemerocher/2007/01/10/my_funky_blog_entry

URL 中的個別代碼會再次對應至 params 物件,其中有可用的 yearmonthdayid 等值。

動態控制器和動作名稱

變數也可以用於動態建構控制器和動作名稱。事實上,預設的 Grails URL 對應使用此技術

static mappings = {
    "/$controller/$action?/$id?"()
}

在此,控制器、動作和 ID 的名稱會從嵌入在 URL 中的變數 controlleractionid 中隱含取得。

您也可以使用閉包動態解析要執行的控制器名稱和動作名稱

static mappings = {
    "/$controller" {
        action = { params.goHere }
    }
}

選用變數

預設對應的另一個特性是可以在變數尾端加上 ?,使其成為選用代碼。在另一個範例中,此技術可以套用於部落格 URL 對應,以建立更彈性的連結

static mappings = {
    "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

透過此對應,所有這些 URL 都會比對,只有相關參數會填入 params 物件

/graemerocher/2007/01/10/my_funky_blog_entry
/graemerocher/2007/01/10
/graemerocher/2007/01
/graemerocher/2007
/graemerocher

選用檔案副檔名

如果您想要擷取特定路徑的副檔名,則有特殊情況對應

"/$controller/$action?/$id?(.$format)?"()

透過加入 (.$format)? 對應,您可以使用控制器中的 response.format 屬性存取檔案副檔名

def index() {
    render "extension is ${response.format}"
}

任意變數

您也可以透過在傳遞至對應的區塊中設定,從 URL 對應傳遞任意參數至控制器

"/holiday/win" {
     id = "Marrakech"
     year = 2007
}

這些變數會在傳遞至控制器的 params 物件中提供。

動態解析變數

硬式編碼的任意變數很有用,但有時您需要根據執行時間因素計算變數名稱。透過將區塊指定給變數名稱,也可以做到這一點

"/holiday/win" {
     id = { params.id }
     isEligible = { session.user != null } // must be logged in
}

在上述情況中,區塊內的程式碼會在實際比對 URL 時解析,因此可以與各種邏輯結合使用。

7.3.5 對應至檢視

您可以解析 URL 至檢視,而無需涉及控制器或動作。例如,要將根 URL / 對應至位於 grails-app/views/index.gsp 位置的 GSP,您可以使用

static mappings = {
    "/"(view: "/index")  // map the root URL
}

或者,如果你需要一個特定於給定控制器的檢視,你可以使用

static mappings = {
   "/help"(controller: "site", view: "help") // to a view for a controller
}

7.3.6 映射到回應代碼

Grails 也讓你將 HTTP 回應代碼映射到控制器、動作或檢視。只要使用與你感興趣的回應代碼相符的方法名稱

static mappings = {
   "403"(controller: "errors", action: "forbidden")
   "404"(controller: "errors", action: "notFound")
   "500"(controller: "errors", action: "serverError")
}

或者你可以指定自訂錯誤頁面

static mappings = {
   "403"(view: "/errors/forbidden")
   "404"(view: "/errors/notFound")
   "500"(view: "/errors/serverError")
}

宣告式錯誤處理

此外,你可以為個別例外狀況設定處理常式

static mappings = {
   "403"(view: "/errors/forbidden")
   "404"(view: "/errors/notFound")
   "500"(controller: "errors", action: "illegalArgument",
         exception: IllegalArgumentException)
   "500"(controller: "errors", action: "nullPointer",
         exception: NullPointerException)
   "500"(controller: "errors", action: "customException",
         exception: MyException)
   "500"(view: "/errors/serverError")
}

有了這個設定,IllegalArgumentException 將會由 ErrorsController 中的 illegalArgument 動作處理,NullPointerException 將會由 nullPointer 動作處理,而 MyException 將會由 customException 動作處理。其他例外狀況將會由 catch-all 規則處理,並使用 /errors/serverError 檢視。

你可以使用請求的 exception 屬性從你的自訂錯誤處理檢視或控制器動作存取例外狀況,如下所示

class ErrorsController {
    def handleError() {
        def exception = request.exception
        // perform desired processing to handle the exception
    }
}
如果你的錯誤處理控制器動作也擲出例外狀況,你將會得到一個 StackOverflowException

7.3.7 映射到 HTTP 方法

URL 映射也可以設定為根據 HTTP 方法 (GET、POST、PUT 或 DELETE) 進行映射。這對於 RESTful API 和根據 HTTP 方法限制映射非常有用。

以下映射提供 ProductController 的 RESTful API URL 映射,作為範例

static mappings = {
   "/product/$id"(controller:"product", action: "update", method: "PUT")
}

請注意,如果你在 URL 映射中指定 GET 以外的 HTTP 方法,你還必須在建立對應連結時指定它,方法是將 method 參數傳遞給 g:linkg:createLink 以取得所需格式的連結。

7.3.8 映射萬用字元

Grails 的 URL 映射機制也支援萬用字元映射。例如,考慮以下映射

static mappings = {
    "/images/*.jpg"(controller: "image")
}

這個映射將會符合所有到影像的途徑,例如 /image/logo.jpg。當然,你可以使用變數達成相同的目的

static mappings = {
    "/images/$name.jpg"(controller: "image")
}

不過,你也可以使用雙重萬用字元來符合多於一個層級

static mappings = {
    "/images/**.jpg"(controller: "image")
}

在這種情況下,映射將會符合 /image/logo.jpg/image/other/logo.jpg。更好的是,你可以使用雙重萬用字元變數

static mappings = {
    // will match /image/logo.jpg and /image/other/logo.jpg
    "/images/$name**.jpg"(controller: "image")
}

在這種情況下,它將會把萬用字元符合的途徑儲存在一個可以從 params 物件取得的 name 參數中

def name = params.name
println name // prints "logo" or "other/logo"

如果您使用萬用字元 URL 對應,您可能會想要從 Grails 的 URL 對應程序中排除某些 URI。為此,您可以在 UrlMappings.groovy 類別中提供 excludes 設定

class UrlMappings {
    static excludes = ["/images/*", "/css/*"]
    static mappings = {
        ...
    }
}

在此情況下,Grails 就不會嘗試對應任何以 /images/css 開頭的 URI。

7.3.9 自動連結重寫

URL 對應的另一個優點是,它們會自動自訂 link 標籤的行為,因此變更對應時,您不需要變更所有連結。

這是透過 URL 重寫技術完成的,此技術會從 URL 對應中反向建構連結。因此,假設有一個對應,例如先前的區段中的部落格對應

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

如果您如下使用 link 標籤

<g:link controller="blog" action="show"
        params="[blog:'fred', year:2007]">
    My Blog
</g:link>

<g:link controller="blog" action="show"
        params="[blog:'fred', year:2007, month:10]">
    My Blog - October 2007 Posts
</g:link>

Grails 會自動以正確格式重寫 URL

<a href="/fred/2007">My Blog</a>
<a href="/fred/2007/10">My Blog - October 2007 Posts</a>

7.3.10 套用約束

URL 對應也支援 Grails 的統一 驗證約束 機制,讓您可以進一步「約束」URL 的對應方式。例如,如果我們重新檢視先前的部落格範例程式碼,目前的對應如下所示

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

這允許使用下列 URL

/graemerocher/2007/01/10/my_funky_blog_entry

不過,它也會允許

/graemerocher/not_a_year/not_a_month/not_a_day/my_funky_blog_entry

這是有問題的,因為它會強制您在控制器程式碼中進行一些巧妙的剖析。幸運的是,URL 對應可以受到約束,以進一步驗證 URL 標記

"/$blog/$year?/$month?/$day?/$id?" {
     controller = "blog"
     action = "show"
     constraints {
          year(matches:/\\\d{4}/)
          month(matches:/\\\d{2}/)
          day(matches:/\\\d{2}/)
     }
}

在此情況下,約束會確保 yearmonthday 參數符合特定的有效模式,因此可以減輕您後續的負擔。

7.3.11 命名 URL 對應

URL 對應也支援命名對應,也就是具有關聯名稱的對應。產生連結時,可以使用名稱來參考特定對應。

定義命名對應的語法如下

static mappings = {
   name <mapping name>: <url pattern> {
      // ...
   }
}

例如

static mappings = {
    name personList: "/showPeople" {
        controller = 'person'
        action = 'list'
    }
    name accountDetails: "/details/$acctNumber" {
        controller = 'product'
        action = 'accountDetails'
    }
}

對應可以在 GSP 中的 link 標籤中參照。

<g:link mapping="personList">List People</g:link>

這將產生

<a href="/showPeople">List People</a>

可以使用 params 屬性指定參數。

<g:link mapping="accountDetails" params="[acctNumber:'8675309']">
    Show Account
</g:link>

這將產生

<a href="/details/8675309">Show Account</a>

或者,您可以使用 link 名稱空間來參照命名對應。

<link:personList>List People</link:personList>

這將產生

<a href="/showPeople">List People</a>

link 名稱空間方法允許將參數指定為屬性。

<link:accountDetails acctNumber="8675309">Show Account</link:accountDetails>

這將產生

<a href="/details/8675309">Show Account</a>

若要指定應該套用至產生的 href 的屬性,請將 Map 值指定至 attrs 屬性。這些屬性會直接套用至 href,不會傳遞以用作要求參數。

<link:accountDetails attrs="[class: 'fancy']" acctNumber="8675309">
    Show Account
</link:accountDetails>

這將產生

<a href="/details/8675309" class="fancy">Show Account</a>

7.3.12 自訂 URL 格式

預設的 URL 對應機制支援 URL 中的駝峰式命名。存取名為 addNumbers 的動作,位於名為 MathHelperController 的控制器中的預設 URL 會類似於 /mathHelper/addNumbers。Grails 允許自訂此模式,並提供一個實作,將駝峰式慣例替換為連字號慣例,支援類似 /math-helper/add-numbers 的 URL。若要啟用連字號 URL,請在 grails-app/conf/application.groovy 中將 grails.web.url.converter 屬性指定為「連字號」。

grails-app/conf/application.groovy
grails.web.url.converter = 'hyphenated'

可以透過提供實作 UrlConverter 介面的類別,並將該類別的執行個體新增至 Spring 應用程式內容,其 bean 名稱為 grails.web.UrlConverter.BEAN_NAME,來插入任意策略。如果 Grails 在內容中找到具有該名稱的 bean,則會將其用作預設轉換器,而且不需要將值指定給 grails.web.url.converter 設定屬性。

src/main/groovy/com/myapplication/MyUrlConverterImpl.groovy
package com.myapplication

class MyUrlConverterImpl implements grails.web.UrlConverter {

    String toUrlElement(String propertyOrClassName) {
        // return some representation of a property or class name that should be used in URLs...
    }
}
grails-app/conf/spring/resources.groovy
beans = {
    "${grails.web.UrlConverter.BEAN_NAME}"(com.myapplication.MyUrlConverterImpl)
}

7.3.13 命名空間控制器

如果應用程式在不同的套件中定義多個具有相同名稱的控制器,則必須在命名空間中定義控制器。定義控制器命名空間的方式是在控制器中定義名為 namespace 的靜態屬性,並將代表命名空間的字串指定給該屬性。

grails-app/controllers/com/app/reporting/AdminController.groovy
package com.app.reporting

class AdminController {

    static namespace = 'reports'

    // ...
}
grails-app/controllers/com/app/security/AdminController.groovy
package com.app.security

class AdminController {

    static namespace = 'users'

    // ...
}

定義應與命名空間控制器關聯的 URL 對應時,namespace 變數需要是 URL 對應的一部分。

grails-app/controllers/UrlMappings.groovy
class UrlMappings {

    static mappings = {
        '/userAdmin' {
            controller = 'admin'
            namespace = 'users'
        }

        '/reportAdmin' {
            controller = 'admin'
            namespace = 'reports'
        }

        "/$namespace/$controller/$action?"()
    }
}

反向 URL 對應也需要指定 namespace

<g:link controller="admin" namespace="reports">Click For Report Admin</g:link>
<g:link controller="admin" namespace="users">Click For User Admin</g:link>

將 URL 對應(正向或反向)解析至命名空間控制器時,只有在已提供 namespace 的情況下,對應才會相符。如果應用程式在不同的套件中提供多個具有相同名稱的控制器,其中最多只能有 1 個未定義 namespace 屬性。如果有多個具有相同名稱的控制器未定義 namespace 屬性,框架將無法區分正向或反向對應解析的控制器。

允許應用程式使用提供與應用程式提供的控制器具有相同名稱的控制器的外掛程式,而且這兩個控制器都不會定義 namespace 屬性,只要控制器位於不同的套件中即可。例如,應用程式可能包含名為 com.accounting.ReportingController 的控制器,而且應用程式可能使用提供名為 com.humanresources.ReportingController 的控制器的外掛程式。唯一的問題是外掛程式提供的控制器的 URL 對應需要明確指定對應套用到外掛程式提供的 ReportingController

請參閱以下範例。

static mappings = {
    "/accountingReports" {
        controller = "reporting"
    }
    "/humanResourceReports" {
        controller = "reporting"
        plugin = "humanResources"
    }
}

有了這個對應,/accountingReports 的要求將由應用程式中定義的 ReportingController 處理。/humanResourceReports 的要求將由 humanResources 外掛程式提供的 ReportingController 處理。

可以由任意數量的外掛程式提供任意數量的 ReportingController 控制器,但即使是在不同的套件中定義,外掛程式也不得提供多於一個 ReportingController

如果應用程式和/或外掛程式在執行時期提供多個具有相同名稱的控制器,則僅需要在對應中指定 `plugin` 變數的值。如果 `humanResources` 外掛程式提供 `ReportingController`,且在執行時期沒有其他可用的 `ReportingController`,則下列對應會有效。

static mappings = {
    "/humanResourceReports" {
        controller = "reporting"
    }
}

最好明確指出控制器是由外掛程式提供的。

7.4 CORS

Spring Boot 提供開箱即用的 CORS 支援,但由於 URL 對應使用的方式,而不是定義 URL 的註解,因此難以在 Grails 應用程式中進行設定。從 Grails 3.2.1 開始,我們新增了一種在 Grails 應用程式中設定 CORS 的方式。

啟用後,預設設定為「完全開放」。

application.yml
grails:
    cors:
        enabled: true

這會產生對應至所有 URL 的對應 `/**`,其中包含

allowedOrigins

['*']

allowedMethods

['*']

allowedHeaders

['*']

exposedHeaders

null

maxAge

1800

allowCredentials

false

其中一些設定直接來自 Spring Boot,且可能在未來版本中變更。請參閱 Spring CORS 設定文件

所有這些設定都可以輕鬆覆寫。

application.yml
grails:
    cors:
        enabled: true
        allowedOrigins:
            - https://127.0.0.1:5000

在上述範例中,`allowedOrigins` 設定會取代 `[*]`。

您也可以設定不同的 URL。

application.yml
grails:
    cors:
        enabled: true
        allowedHeaders:
            - Content-Type
        mappings:
            '[/api/**]':
                allowedOrigins:
                    - https://127.0.0.1:5000
                # Other configurations not specified default to the global config

請注意,對應金鑰必須使用方括號表示法建立 (請參閱 https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#map-based-binding),這是 Spring Boot 1.5 (Grails 3) 和 Spring Boot 2 (Grails 4) 之間的重大變更。

指定至少一個對應會停用建立全域對應 (/**)。如果您希望保留該設定,您應該連同其他對應一起指定它。

上述設定會產生單一對應 `api/**`,其設定如下

allowedOrigins

['https://127.0.0.1:5000']

allowedMethods

['*']

allowedHeaders

['Content-Type']

exposedHeaders

null

maxAge

1800

allowCredentials

false

如果您不希望覆寫任何預設設定,但只想要指定 URL,您可以像這個範例一樣執行

application.yml
grails:
    cors:
        enabled: true
        mappings:
            '[/api/**]': inherit

7.5 攔截器

Grails 使用 create-interceptor 指令提供獨立的攔截器

$ grails create-interceptor MyInterceptor

上述指令會在 `grails-app/controllers` 目錄中建立一個攔截器,其預設內容如下

class MyInterceptor {

  boolean before() { true }

  boolean after() { true }

  void afterView() {
    // no-op
  }

}

攔截器與篩選器

在 Grails 3.0 之前的 Grails 版本中,Grails 支援過濾器的概念。這些過濾器仍受支援以維持向後相容性,但已視為已不建議使用。

Grails 3.0 中新的攔截器概念在許多方面都較為優越,其中最重要的部分是攔截器可以使用 Groovy 的 CompileStatic 註解來最佳化效能(這一點通常至關重要,因為攔截器可以針對每個要求執行。)

7.5.1 定義攔截器

預設情況下,攔截器會比對具有相同名稱的控制器。例如,如果您有一個名為 BookInterceptor 的攔截器,則對 BookController 動作的所有要求都會觸發攔截器。

Interceptor 實作 Interceptor 特質,並提供 3 種可用於攔截要求的方法

/**
 * Executed before a matched action
 *
 * @return Whether the action should continue and execute
 */
boolean before() { true }

/**
 * Executed after the action executes but prior to view rendering
 *
 * @return True if view rendering should continue, false otherwise
 */
boolean after() { true }

/**
 * Executed after view rendering completes
 */
void afterView() {}

如上所述,before 方法會在動作之前執行,並可透過傳回 false 來取消動作的執行。

after 方法會在動作執行後執行,如果傳回 false,則可以停止檢視呈現。after 方法也可以分別使用 viewmodel 屬性來修改檢視或模型

boolean after() {
  model.foo = "bar" // add a new model attribute called 'foo'
  view = 'alternate' // render a different view called 'alternate'
  true
}

afterView 方法會在檢視呈現完成後執行。如果發生例外狀況,則可以使用 Interceptor 特質的 throwable 屬性取得例外狀況。

7.5.2 使用攔截器比對要求

如前一節所述,預設情況下,攔截器只會比對與相關控制器相關聯的要求。不過,您可以使用 Interceptor API 中定義的 matchmatchAll 方法,將攔截器設定為比對任何要求。

比對方法會傳回 Matcher 執行個體,可使用此執行個體設定攔截器比對要求的方式。

例如,下列攔截器會比對除 login 控制器之外的所有要求

class AuthInterceptor {
  AuthInterceptor() {
    matchAll()
    .excludes(controller:"login")
  }

  boolean before() {
    // perform authentication
  }
}

您也可以使用命名參數進行比對

class LoggingInterceptor {
  LoggingInterceptor() {
    match(controller:"book", action:"show") // using strings
    match(controller: ~/(author|publisher)/) // using regex
  }

  boolean before() {
    ...
  }
}

您可以在攔截器中使用任意數量的比對器。它們會按照定義順序執行。例如,上述攔截器會比對以下所有項目

  • 當呼叫 BookControllershow 動作時

  • 當呼叫 AuthorControllerPublisherController

除了 uri 之外,所有命名參數都接受字串或正規表示式。uri 參數支援與 Spring 的 AntPathMatcher 相容的字串路徑。可能的命名參數如下

  • namespace - 控制器命名空間

  • controller - 控制器名稱

  • action - 動作名稱

  • method - HTTP 方法

  • uri - 要求的 URI。如果使用此參數,則其他所有參數都將被忽略,並且僅使用此參數。

7.5.3 排序攔截器執行

可以透過定義優先順序的 order 屬性來排序攔截器。

例如

class AuthInterceptor {

  int order = HIGHEST_PRECEDENCE

  ...
}

order 屬性的預設值為 0。攔截器執行順序是根據 order 屬性以遞增順序排序,並先執行數字順序最低的攔截器。

HIGHEST_PRECEDENCELOWEST_PRECEDENCE 值可用於定義應分別先或後執行的篩選器。

請注意,如果您撰寫的攔截器供其他人使用,最好增加或減少 HIGHEST_PRECEDENCELOWEST_PRECEDENCE,以允許其他攔截器插入在您撰寫的攔截器之前或之後

int order = HIGHEST_PRECEDENCE + 50

// or

int order = LOWEST_PRECEDENCE - 50

若要找出攔截器的計算順序,您可以將偵錯記錄器新增到 logback.groovy,如下所示

logger 'grails.artefact.Interceptor', DEBUG, ['STDOUT'], false

您可以使用 grails-app/conf/application.yml 中的 bean 覆寫設定來覆寫任何攔截器的預設順序

beans:
  authInterceptor:
    order: 50

或在 grails-app/conf/application.groovy

beans {
  authInterceptor {
    order = 50
  }
}

這樣可以讓您完全控制攔截器執行順序。

7.6 內容協商

Grails 內建支援 內容協商,使用 HTTP Accept 標頭、明確的格式要求參數或已對應 URI 的延伸模組。

設定 Mime 類型

在開始處理內容協商之前,您需要告訴 Grails 您希望支援哪些內容類型。預設情況下,Grails 已使用 grails.mime.types 設定在 grails-app/conf/application.yml 中設定多種不同的內容類型

grails:
    mime:
        types:
            all: '*/*'
            atom: application/atom+xml
            css: text/css
            csv: text/csv
            form: application/x-www-form-urlencoded
            html:
              - text/html
              - application/xhtml+xml
            js: text/javascript
            json:
              - application/json
              - text/json
            multipartForm: multipart/form-data
            rss: application/rss+xml
            text: text/plain
            hal:
              - application/hal+json
              - application/hal+xml
            xml:
              - text/xml
              - application/xml

設定也可以在 grails-app/conf/application.groovy 中完成,如下所示

grails.mime.types = [ // the first one is the default format
    all:           '*/*', // 'all' maps to '*' or the first available format in withFormat
    atom:          'application/atom+xml',
    css:           'text/css',
    csv:           'text/csv',
    form:          'application/x-www-form-urlencoded',
    html:          ['text/html','application/xhtml+xml'],
    js:            'text/javascript',
    json:          ['application/json', 'text/json'],
    multipartForm: 'multipart/form-data',
    rss:           'application/rss+xml',
    text:          'text/plain',
    hal:           ['application/hal+json','application/hal+xml'],
    xml:           ['text/xml', 'application/xml']
]

上述設定片段允許 Grails 將包含「text/xml」或「application/xml」媒體類型的要求格式偵測為「xml」。您可以透過將新項目新增到對應中來新增自己的類型。第一個是預設格式。

使用格式請求參數進行內容協商

假設控制器動作可以回傳各種格式的資源:HTML、XML 和 JSON。客戶端將取得哪種格式?最簡單且最可靠的方式是讓客戶端透過 format URL 參數來控制這項功能。

因此,如果您作為瀏覽器或其他客戶端,想要以 XML 格式取得資源,可以使用類似這樣的 URL

http://my.domain.org/books.xml
請求參數 format 允許 http://my.domain.org/books?format=xml,但預設的 Grails URL 對應 get "/$controller(.$format)?"(action:"index") 會使用 null 覆寫 format 參數。因此,預設對應應更新為 get "/$controller"(action:"index")

在伺服器端,這會在 response 物件上產生一個 format 屬性,其值為 xml

您也可以在 URL 對應 定義中定義這個參數

"/book/list"(controller:"book", action:"list") {
    format = "xml"
}

您可以編寫控制器動作,根據這個屬性回傳 XML,但您也可以使用特定於控制器的 withFormat() 方法

這個範例需要新增 org.grails.plugins:grails-plugin-converters 外掛程式
import grails.converters.JSON
import grails.converters.XML

class BookController {

    def list() {
        def books = Book.list()

        withFormat {
            html bookList: books
            json { render books as JSON }
            xml { render books as XML }
            '*' { render books as JSON }
        }
    }
}

在此範例中,Grails 只會執行與請求內容類型相符的 withFormat() 內的區塊。因此,如果偏好的格式是 html,Grails 只會執行 html() 呼叫。每個「區塊」可以是對應檢視的模型對應 (就像我們在上述範例中對「html」所做的那樣),或是一個封閉區塊。封閉區塊可以包含任何標準動作程式碼,例如,它可以回傳一個模型或直接呈現內容。

當沒有格式明確相符時,可以使用 * (萬用字元) 區塊來處理所有其他格式。

有一個特殊格式「all」,其處理方式與明確格式不同。如果指定「all」(通常透過 Accept 標頭進行 - 請參閱下方),則當沒有可用的 * (萬用字元) 區塊時,會執行 withFormat() 的第一個區塊。

您不應新增明確的「all」區塊。在此範例中,「all」格式會觸發 html 處理常式 (html 是第一個區塊,而且沒有 * 區塊)。

withFormat {
    html bookList: books
    json { render books as JSON }
    xml { render books as XML }
}
使用 withFormat 時,請確保它是控制器動作中的最後一個呼叫,因為 withFormat 方法的回傳值會由動作用來決定接下來發生什麼事。

使用 Accept 標頭

每個傳入的 HTTP 要求都有一個特殊的 Accept 標頭,用來定義用戶端可以「接受」的媒體類型 (或 MIME 類型)。在較舊的瀏覽器中,這通常是

*/*

這表示任何東西。不過,較新的瀏覽器會傳送更有趣的數值,例如 Firefox 傳送的這個

text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, \
    text/plain;q=0.8, image/png, */*;q=0.5

這個特定的 Accept 標頭沒有幫助,因為它表示 XML 是偏好的回應格式,而用戶實際上預期的是 HTML。這就是 Grails 預設忽略瀏覽器的 Accept 標頭的原因。不過,非瀏覽器用戶端通常會在它們的要求中更具體,並可以傳送像這樣的 Accept 標頭

application/json

如前所述,Grails 中的預設組態是忽略瀏覽器的 Accept 標頭。這是透過組態設定 grails.mime.disable.accept.header.userAgents 來完成的,它組態為偵測主要的渲染引擎並忽略它們的 ACCEPT 標頭。這讓 Grails 的內容協商可以繼續對非瀏覽器用戶端運作

grails.mime.disable.accept.header.userAgents = ['Gecko', 'WebKit', 'Presto', 'Trident']

例如,如果它看到上面的 Accept 標頭 ('application/json'),它會將 format 設定為 json,正如你所預期的。當然,這與 withFormat() 方法的運作方式相同,就像設定 format URL 參數時一樣 (儘管 URL 參數優先)。

Accept 標頭為 '*/\*' 會導致 format 屬性的值為 all

如果使用了 Accept 標頭,但它不包含任何已註冊的內容類型,Grails 會假設傳送要求的是一個損壞的瀏覽器,並會設定 HTML 格式 - 請注意,這與其他內容協商模式的運作方式不同,因為這些模式會啟用「all」格式!

要求格式與回應格式

從 Grails 2.0 開始,要求格式和回應格式有不同的概念。要求格式由 CONTENT_TYPE 標頭決定,通常用於偵測傳入的要求是否可以解析為 XML 或 JSON,而回應格式使用檔案副檔名、格式參數或 ACCEPT 標頭來嘗試傳送適當的回應給用戶端。

控制器上可用的 withFormat 特別處理回應格式。如果你希望加入處理要求格式的邏輯,可以使用要求上可用的另一個 withFormat 方法來做到

request.withFormat {
    xml {
        // read XML
    }
    json {
        // read JSON
    }
}

使用 URI 副檔名的內容協商

Grails 也支援使用 URI 副檔名進行內容協商。例如,給定以下 URI

/book/list.xml

這是因為預設的 URL 對應定義是

"/$controller/$action?/$id?(.$format)?"{

請注意路徑中包含 format 變數。如果您不想透過檔案副檔名使用內容協商,請直接移除 URL 對應中的這部分

"/$controller/$action?/$id?"{

測試內容協商

若要在單元或整合測試中測試內容協商(請參閱 測試 章節),您可以操作輸入的請求標頭

void testJavascriptOutput() {
    def controller = new TestController()
    controller.request.addHeader "Accept",
              "text/javascript, text/html, application/xml, text/xml, */*"

    controller.testAction()
    assertEquals "alert('hello')", controller.response.contentAsString
}

或者,您可以設定格式參數以達成類似的效果

void testJavascriptOutput() {
    def controller = new TestController()
    controller.params.format = 'js'

    controller.testAction()
    assertEquals "alert('hello')", controller.response.contentAsString
}