(快速参考)

7 Web 层

版本 6.2.0

7 Web 层

7.1 控制器

控制器处理请求并创建或准备响应。控制器可以直接生成响应或委托给视图。要创建一个控制器,只需创建一个 类,其名称以 Controller 结尾,位于 grails-app/controllers 目录(如果是在包中,则位于子目录中)。

默认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
    }
}

由于属性被命名为 list,因此此示例默认映射到 /book/list URI。

默认操作

控制器具有默认 URI 的概念,该 URI 映射到控制器的根 URI,例如 BookController/book。根据以下规则规定了在请求默认 URI 时调用的操作

  • 如果只有一个操作,则为默认操作

  • 如果您有一个名为 index 的操作,则为默认操作

  • 您也可以使用 defaultAction 来明确设置它

static defaultAction = "list"

7.1.2 控制器和范围

可用范围

范围是类哈希的对象,您可以在其中存储变量。以下范围可用于控制器

  • servletContext - 也称为应用程序范围,该范围允许您在整个 Web 应用程序中共享状态。servletContext 是 ServletContext 的一个实例

  • session - 会话允许关联状态与给定的用户,会话通常使用 Cookie 将会话与客户端关联。会话对象是 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"

可以在 application.yml 中使用 grails.controllers.defaultScope 键定义默认策略,例如

grails:
    controllers:
        defaultScope: singleton
明智地使用作用域化控制器。例如,我们不建议在单例作用域的控制器中具有任何属性,因为它们将针对所有请求共享。

7.1.3 模型和视图

返回模型

模型是一个在渲染时视图使用的映射。该映射中的键与视图可访问的变量名称对应。有几种方法可以返回模型。首先,可以显式返回一个映射实例的

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 将在这个show 动作中,在位置grails-app/views/book/show.gsp寻找视图

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 实现所需的依赖项。

选择命名空间控制器的视图

如果控制器用namespace属性为自身定义命名空间,则这将影响 Grails 查找以相对路径指定视图的根目录。命名空间控制器呈现的视图的默认根目录为grails-app/views/<namespace name>/<controller name>/。如果在命名空间目录中找不到视图,则 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 的 MarkupBuilder 来为 render 方法生成 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: "https://grails.groovy-lang.cn")
// 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 map 来访问模型。只有在调用 chain 方法后的操作中才存在此动态属性

class ChainController {

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

redirect 方法一样,您还可以将参数传递到 chain 方法

chain(action: "action1", model: [one: 1], params: [myparam: "param1"])
chain 方法使用 HTTP 会话,因此仅当您的应用程序有状态时才应使用它。

7.1.5 数据绑定

数据绑定是将传入请求参数“绑定”到对象的属性或对象的整个图上的操作。数据绑定应处理所有必需的类型转换,因为请求参数(通常通过表单提交传递)始终为字符串,而 Groovy 或 Java 对象的属性可能并非如此。

基于 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 类型的 Map 来填充一整个对象图。

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 中元素的顺序相对应。在上文的代码示例中,如果 albums 是一个 Set 而不是一个 List,那么 bindingMap 看上去可能会完全一样,但是“Foxtrot”可能是 Set 中的第一张专辑,也可能是第二张。当更新 Set 中的现有元素时,赋值给 SetMap 中必须包含表示要更新的 Set 中元素的 id 元素,如下例所示:

/*
 * 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,以便数据绑定程序使用。例如,如果一个请求包含分别为 'USA' 和 'St. Louis' 的名为 person.homeAddress.countryperson.homeAddress.city 的请求参数,那么 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提交为请求参数。当此行为不可取时,应用程序可以直接分配值。

mass属性绑定机制默认情况下将在绑定时自动截取所有字符串。要禁用此行为,请将grails-app/conf/application.groovy中的grails.databinding.trimStrings属性设置为false。

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

// ...

mass属性绑定机制默认情况下将在绑定时自动将所有空字符串转换为null。要禁用此行为,请将grails-app/conf/application.groovy中的grails.databinding.convertEmptyStringsToNull属性设置为false。

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

// ...

时间的顺序是:先进行字符串截取,然后进行空转换,所以如果trimStringstrueconvertEmptyStringsToNulltrue,不仅空字符串会转换为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

数据绑定与多端关联

如果您有一对多或多对多关联,则存在不同的技术可根据关联类型进行数据绑定。

如果您有一个基于 Set 的关联(对 Set 而言这是默认值),则填充关联的最简单方法是发送一个标识符列表。例如,请考虑在下面使用 <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': '']"/>

这会将所选图像绑定到 images 这个 Map 属性中,键名为 "cover"

在绑定到 Map、Array 或 Collection 时,数据绑定器会自动根据需要增大集合的大小。

绑定程序将增大集合的默认限制是 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 和数字 0),并将相应的错误添加到定义控制器的 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 注释应用于 Date 字段时,可指定自定义的日期格式,供将 String 绑定到 Date 值时使用。

import grails.databinding.BindingFormat

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

可在 application.groovy 中配置全局设置,以定义在绑定到 Date 时将在整个应用程序中使用的日期格式。

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 中指定的格式将按照其在 List 中包含的顺序进行尝试。如果某个属性标记为 @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 注释的值转换 String 大小的简单自定义格式化器的示例。

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 注释可以应用于 String 字段,以通知数据绑定器利用自定义转换器。

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_month、birthday_date 和 birthday_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 字段。一个 Shape 有一个 area 属性。这个应用程序可能要接收请求参数,例如 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 接口,并为接口中所有方法提供了默认实现,因此这个类非常适合用来做子类化,省去您的监听器类只需要提供实现监听器感兴趣的方法即可。

直接使用数据绑定器

有些情况下,某个应用程序可能想要直接使用数据绑定器。例如,在 Service 中对某个不是领域类的任意对象执行绑定。以下内容不适用于只读的 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
    }

}

有关 bind 方法的重载版本的更多信息,请参阅 DataBinder 文档。

数据绑定和安全问题

从请求参数批量更新属性时,需要小心不要让客户端将恶意数据绑定到领域类并保存在数据库中。可以使用下标运算符限制绑定到给定领域类的属性

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 方法将为对象寻找合适的 呈现器 以及从 呈现器注册表 计算的媒体类型。

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 媒体类型时,这些视图将被使用。这样,你可以定义一个后端,既能为 Web 浏览器提供响应,又能表示应用程序的 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 接口上传文件。文件上传的第一步是创建这样的多分部表单

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

uploadForm 标签方便地为标准 <g:form> 标签添加了 enctype="multipart/form-data" 属性。

接下来有几种处理文件上传的方式。一种是直接处理 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 属性的类型更改为 String 类型来设置文件内容。

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 = 允许的多部分/表单数据请求的最大大小。

将文件大小限制到最大值,以防止拒绝服务攻击。

这些限制的存在是为了防止拒绝服务攻击并提升整体应用程序性能

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 特性允许定义 Constraints,就像在 域类中所做的那样。如果在与控制器相同源文件中定义了命令对象,Grails 将自动使其变为 Validateable。命令对象类不需要是可以验证的。

默认情况下,所有 Validateable 对象属性(不是 java.util.Collectionjava.util.Map 的实例)都是 nullable: falsejava.util.Collectionjava.util.Map 的实例默认为 nullable: true。如果希望有一个 Validateable 默认具有 nullable: true 属性,则可以通过在类中定义 defaultNullable 方法来指定。

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

    static boolean defaultNullable() {
        true
    }
}

在本例中,nameage 在验证期间允许为 null 值。

使用命令对象

要使用命令对象,控制器动作可以选择指定任何数量的命令对象参数。必须提供参数类型以便 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)
        }
    }
}

在此示例中,命令对象与 loginService bean 交互,该 bean 通过名称从 Spring ApplicationContext 注入。

将请求正文绑定到命令对象

当向控制器操作发出请求时,它接受命令对象并且该请求包含正文部分,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 具有使用“同步标记模式”处理重复表单提交的内置支持。要开始使用,请在 form 标记上定义一个标记

<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 服务器页面

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 映射来定义映射,可以在任何 URL 映射前加上 HTTP 方法名称(小写)来表示它适用的 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")

注意,在每个 URL 映射定义之前都添加了 HTTP 方法名称。

单一资源

单一资源是一个仅存在一个(可能因用户而异)资源的系统资源。你可以使用 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

主要的区别是 URL 映射中不包含 id。

嵌套资源

你可以嵌套资源映射来生成子资源。例如

"/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>

但是,为了让此更加简洁,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 将自动将第二个标记映射到一个称为 id 的参数(可通过 params 对象访问)。例如,给定 URL /product/MacBook,以下代码将向响应呈现“MacBook”

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

当然,您可以构建更复杂的映射示例。例如,传统的 blog 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 方法

还可以根据 HTTP 方法(GET、POST、PUT 或 DELETE)配置 URL 映射。这对 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")
}

如果您按如下方式使用链接标记

<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 中的链接标记中引用。

<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 的属性,请为 attrs 属性指定一个 Map 值。这些属性将直接应用到 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 中的驼峰名称。用于访问 MathHelperController 控制器中名为 addNumbers 的操作的默认 URL 类似于 /mathHelper/addNumbers。Grails 允许自定义此模式,并提供用连字符约定替换驼峰约定、支持 /math-helper/add-numbers 之类 URL 的实现。若要启用连字符 URL,请将 grails.web.url.converter 属性分配给 grails-app/conf/application.groovy 中的“连字符”值。

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

任意策略可以通过提供一个实现 UrlConverter 接口的类,并使用 grails.web.UrlConverter.BEAN_NAME 的 bean 名称将该类的实例添加到 Spring 应用环境中插入。如果 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"
    }
}

设置该映射后,将由应用程序中定义的 ReportingController 来处理对 /accountingReports 的请求。将由 humanResources 插件提供的 ReportingController 来处理对 /humanResourceReports 的请求。

任何数量的插件可以提供任意数量的 ReportingController 控制器,但即使它们定义在单独的包中,也没有任何插件可以提供多个 ReportingController

只有在应用程序和/或插件在运行时提供了多个同名控制器时,才需要在映射中为 plugin 变量分配值。如果 humanResources 插件提供一个 ReportingController,并且在运行时没有其他可用的 ReportingController,则适用于以下映射。

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

最好明确表示该控制器是由插件提供的。

7.4 CORS

Spring Boot 开箱即用地提供了 CORS 支持,但由于使用 UrlMappings 代替了定义 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:
            - http://localhost:5000

在上面的示例中,allowedOrigins 设置将取代 [*]

您还可以配置不同的 URL。

application.yml
grails:
    cors:
        enabled: true
        allowedHeaders:
            - Content-Type
        mappings:
            '[/api/**]':
                allowedOrigins:
                    - http://localhost: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

['http://localhost: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 使用拦截器匹配请求

如前一部分中提到的,通过约定,默认情况下一个拦截器将仅匹配与相关控制器相关的请求。不过你可以使用 拦截器 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-app/conf/application.yml」 中通过使用 「grails.mime.types」 设置配置了许多不同类型的内容

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")」 将用空值覆盖 「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
}