Grails 4 学习 插件之 JSON Views 返回各种JSON数据

颜志学
2023-12-01

在开发grails项目的时候用到了JSON数据的传递解析。但是使用render  ... as JSON 返回的json数据会带转义字符,无奈替换转义字符数据还是不能用,只好看官方文档,于是看到了Grails Views的JSON Views.

这是一个插件,安装配置官方文档都说的很明白,但唯独怎么使用说的不是很清楚。

安装插件后,文档介绍Json数据在 文件后缀为.gson文件中处理。同时可以使用模板来配置复杂的json数据。一切都就绪后,但是不知道怎么调用。研究了一上午,终于搞定了。

比如说我想获得一个user的json数据。那么

  1. 首先编写一个 user.gson
  2. 在user.gson里实现json的内容
  3. 在user controller里面写一个user方法
  4. 最后在user方法里return user.gson所需要的各种数据。
  5. return支持返回多个数据,以键值对方式返回 例如:
    retrun ["users":users,"userCount":count]

     

 

下面是文档介绍。很简单,也很好看明白。

2. JSON Views

2.1. Introduction

JSON views are written in Groovy, end with the file extension gson and reside in the grails-app/views directory. They provide a DSL for producing output in the JSON format. A hello world example can be seen below:

grails-app/views/hello.gson

json.message {
    hello "world"
}

The above JSON view results in the output:

{"message":{ "hello":"world"}}

The json variable is an instance of StreamingJsonBuilder. See the documentation in the Groovy user guide for more information on StreamingJsonBuilder.

More example usages:

json(1,2,3) == "[1,2,3]"
json { name "Bob" } == '{"name":"Bob"}'
json([1,2,3]) { n it } == '[{"n":1},{"n":2},{"n":3}]'

You can specify a model to view in one of two ways. Either with a model block:

grails-app/views/hello.gson

model {
    String message
}
json.message {
    hello message
}

Or with the @Field transformation provided by Groovy:

grails-app/views/hello.gson

import groovy.transform.Field
@Field String message
json.message {
    hello message
}

2.2. Version History

The current release is 2.0.0.RC2. Below is a version history.

1.3.0

  • The includes support has changed in a way that may break existing use cases. Having includes: ['captain', 'captain.name']] previously excluded all other fields except for captain.name. The captain in the list was necessary for captain.name to function as expected. Now the captain entry will cause all fields in captain to be rendered. To restore the previous behavior, simply remove the captain entry. includes: ['captain', 'captain.name'] → includes: ['captain.name].

1.2.0

  • JSON API support

  • Ability to register custom converters

  • New configuration options for date formatting and unicode escaping

  • New testing trait that integrates with the testing-support framework (1.2.5)

1.1.0

  • Global template support

  • Template inheritance

  • Global templates for GORM for MongoDB / GeoJSON

  • Support for easily testing JSON views with JsonViewTest

  • Pagination support in HAL

  • Better Embedded support in HAL

  • Ability to access HTTP parameters and headers

  • Resource expansion with the expand parameter

  • Controller namespace support

  • Support for a default /object/_object.gson template

1.0.0

  • Initial 1.0.0 GA Release

2.3. Installation

To activate JSON views, add the following dependency to the dependencies block of your build.gradle:

compile "org.grails.plugins:views-json:2.0.0.RC2"

If you are also using MongoDB you may want to add the views-json-templates dependency too which includes support for GeoJSON:

compile "org.grails.plugins:views-json-templates:2.0.0.RC2"

To enable Gradle compilation of JSON views for production environment add the following to the buildscriptdependencies block:

buildscript {
    ...
    dependencies {
        ...
        classpath "org.grails.plugins:views-gradle:2.0.0.RC2"
    }
}

Then apply the org.grails.plugins.views-json Gradle plugin after any Grails core gradle plugins:

...
apply plugin: "org.grails.grails-web"
apply plugin: "org.grails.plugins.views-json"

This will add a compileGsonViews task to Gradle that is executed when producing a JAR or WAR file.

2.4. Templates

2.4.1. Template Basics

You can define templates starting with underscore _. For example given the following template called _person.gson:

grails-app/views/person/_person.gson

model {
    Person person
}
json {
    name person.name
    age person.age
}

You can render the template with the g.render method:

grails-app/views/person/show.gson

model {
    Person person
}
json g.render(template:"person", model:[person:person])

The above assumes the view is in the same directory as the template. If this is not the case you may need to use a relative URI to the template:

grails-app/views/family/show.gson

model {
    Person person
}
json g.render(template:"/person/person", model:[person:person])

2.4.2. Template Namespace

The previous example can be simplified using the template namespace:

grails-app/views/person/show.gson

model {
    Person person
}
json tmpl.person(person)

In this example, the name of the method call (person in this case) is used to dictate which template to render.

The argument to the template becomes the model. The name of the model variable is the same as the template name. If you wish to alter this you can pass a map instead:

grails-app/views/person/show.gson

model {
    Person person
}
json tmpl.person(individual:person)

In the above example the model variable passed to the _person.gson template is called individual.

This technique may also be used when you want to render a template using a relative path:

grails-app/views/person/show.gson

model {
    Person person
}
json tmpl.'/person/person'(person:person)

The template namespace even accepts a collection (or any Iterable object):

grails-app/views/person/show.gson

model {
    List<Person> people = []
}
json tmpl.person(people)

In this case the output is a JSON array. For example:

    [{"name":"Fred",age:10},{"name":"Bob",age:12}]

When rendering an Iterable, you can also specify the model name:

grails-app/views/person/show.gson

model {
    List<Person> people = []
}
json tmpl.person("owner", people)

The person template would have a field defined: Person owner

If you need additional data that will be static over each iteration of the template, you can also pass in a model:

grails-app/views/person/show.gson

model {
    List<Person> people = []
}
json tmpl.person(people, [relationship: "Father"])

The person template could have a field defined: String relationship

By passing in a collection the plugin will iterate over each element on the collection and render the template as a JSON array. If you do not want this to happen then use the variation of the method that takes a map instead:

grails-app/views/person/show.gson

model {
    List<Person> people = []
}
json tmpl.person(people:people)

2.4.3. More Ways to Render Templates

The g.render method is flexible, you can render templates in many forms:

model {
    Family family
}
json {
    name family.father.name
    age family.father.age
    oldestChild g.render(template:"person", model:[person: family.children.max { Person p -> p.age } ])
    children g.render(template:"person", collection: family.children, var:'person')
}

However, most of these use cases are more concise with the template namespace:

model {
    Family family
}
json {
    name family.father.name
    age family.father.age
    oldestChild tmpl.person( family.children.max { Person p -> p.age } ] )
    children tmpl.person( family.children )
}

2.4.4. Template Inheritance

JSON templates can inherit from a parent template. For example consider the following parent template:

grails-app/views/_parent.gson

model {
    Object object
}
json {
    hal.links(object)
    version "1.0"
}

A child template can inherit from the above template by using the inherits method:

grails-app/views/_person.gson

inherits template:"parent"
model {
    Person person
}
json {
    name person.name
}

The JSON from the parent and the child template will be combined so that the output is:

 {
   "_links": {
     "self": {
       "href": "http://localhost:8080/person/1",
       "hreflang": "en",
       "type": "application/hal+json"
     }
   },
   "version": "1.0",
   "name": "Fred"
 }

The parent template’s model will be formulated from the child templates model and the super class name. For example if the model is Person person where Person extends from Object then the final model passed to the parent template will look like:

[person:person, object:person]

If the Person class extended from a class called Mammal then the model passed to the parent would be:

[person:person, mammal:person]

This allows you to design your templates around object inheritance.

You can customize the model passed to the parent template using the model argument:

inherits template:"parent", model:[person:person]

2.5. Rendering Domain Classes

2.5.1. Basics of Domain Class Rendering

Typically your model may involve one or many domain instances. JSON views provide a render method for rendering these.

For example given the following domain class:

class Book {
    String title
}

And the following template:

model {
    Book book
}
json g.render(book)

The resulting output is:

{"id":1,"title":"The Stand"}

You can customize the rendering by including or excluding properties:

json g.render(book, [includes:['title']])

Or by providing a closure to provide additional JSON output:

json g.render(book) {
    pages 1000
}

Or combine the two approaches:

json g.render(book, [includes:['title']]) {
    pages 1000
}

2.5.2. Deep Rendering of Domain Classes

Typically the g.render(..) method will only render objects one level deep. In other words if you have a domain class such as:

class Book {
    String title
    Author author
}

The resulting output will be something like:

{"id":1,"title":"The Stand","author":{id:1}}

If you wish for the author to be included as part of the rendering, there are two requirements, first you must make sure the association is initialized.

If the render method encounters a proxy, it will not traverse into the relationship to avoid N+1 query performance problems.

 The same applies to one-to-many collection associations. If the association has not been initialized the render method will not traverse through the collection!

So you must make sure your query uses a join:

Book.findByTitle("The Stand", [fetch:[author:"join"]])

Secondly when calling the render method you should pass the deep argument:

json g.render(book, [deep:true])

Alternatively, to only expand a single association you can use the expand argument:

json g.render(book, [expand:['author']])
 request parameters can also be used to expand associations (eg. ?expand=author), if you do not want to allow this, then use includes or excludes to include only the properties you want.

Finally, if you prefer to handle the rendering yourself you can do by excluding the property:

json g.render(book, [excludes:['author']]) {
    author {
        name book.author.name
    }
}

2.5.3. Domain Class Rendering and Templates

An alternative to the default behaviour of the render method is to rely on templates.

In other words if you create a /author/_author.gson template and then use the g.render method on an instance of book:

json g.render(book)

Whenever the author association is encountered the g.render method will automatically render the /author/_author.gson template instead.

2.6. JSON View API

All JSON views subclass the JsonViewTemplate class by default.

The JsonViewTemplate superclass implements the JsonView trait which in turn extends the the GrailsView trait.

Thanks to these traits several methods and properties are available to JSON views that can be used to accomplish different tasks.

2.6.1. Creating Links

Links can be generated using the g.link(..) method:

json.person {
    name "bob"
    homepage g.link(controller:"person", id:"bob")
}

The g.link method is similar to the equivalent tag in GSP and allows you to easily create links to other controllers.

2.6.2. Altering the Response Headers

To customize content types and headers use the response object defined by the HttpView trait:

response.contentType "application/hal+json"
response.header "Token", "foo"
json.person {
    name "bob"
}

The HttpView trait defines a variety of methods for inspecting the request and altering the response.

 The methods available are only a subset of the methods available via the HttpServletRequest and HttpServletResponse objects, this is by design as view logic should be limited and logic performed in the controller instead.

2.6.3. Accessing the Request

Various aspects of the request can be accessed by the request object defined by the HttpView trait:

json.person {
 name "bob"
 userAgent request.getHeader('User-Agent')
}

Parameters can be accessed via the params object which is an instance of Parameters:

json.person {
 name "bob"
 first params.int('offset', 0)
 sort params.get('sort', 'name')
}

2.6.4. Default Static Imports

The following classes' static properties are imported by default:

  • org.springframework.http.HttpStatus

  • org.springframework.http.HttpMethod

  • grails.web.http.HttpHeaders

This means that you can use the response object to set the status using these constants, instead of hard coded numbers:

response.status NOT_FOUND

Or generate links using the appropriate HTTP method:

g.link(resource:"books", method:POST)

2.6.5. I18n & Locale Integration

You can lookup i18n messages use the g.message method:

json.error {
    description g.message(code:'default.error.message')
}

You can also create locale specific views by appending the locale to view name. For example person_de.gson for German or person.gson for the default.

 For more complex message lookups the messageSource property is an instance of the Spring MessageSource class.

2.6.6. Accessing Configuration

The application configuration is injected automatically into your json views. To access it, simply reference config.

json {
    foo config.getProperty("bar", String, null)
}

2.6.7. Miscellaneous Properties

Other properties are also available:

controllerName

The name of the current controller

actionName

The name of the current controller action

controllerNamespace

The namespace of the current controller

2.7. Model Naming

Grails supports a convention for the model names in your JSON views. If the convention does not meet your needs, model variables can be explicitly defined.

 Some model names are reserved since there are properties of the same name injected into the view:localeresponserequestpagecontrollerNamespacecontrollerNameactionNameconfiggenerator

2.7.1. Explicit Model Naming

Given a view:

grails-app/views/hello/index.gson

model {
    String message
}
json.message {
    hello message
}

Then the controller has to specify the name to be used in the view:

grails-app/controllers/HelloController.groovy

def index() {
    respond(message: "Hello World")
    //or [message: "Hello World"]
}

When using a template:

grails-app/views/hello/_msg.gson

model {
    String message
}
json.message {
    hello message
}

In the view you can use the tmpl namespace:

json {
    message tmpl.msg([message: message])
    // or g.render(template:'msg', model:[message: message])
    // or g.render(template:'msg', model: message, var:'message')
}

Using collections:

model {
    List<String> messages
}
json {
    messages tmpl.msg('message', messages)
    // or g.render(template:'msg', collection: messages, var: 'message')
}

2.7.2. Model By Convention

Property Type

When rendering a non iterable object, the property name of the type is used when a name is not specified.

grails-app/views/hello/index.gson

model {
    String string
}
json.message {
    hello string
}

The variable can be passed in directly to respond.

grails-app/controllers/HelloController.groovy

def index() {
    respond("Hello World")
}

This also applies when rendering templates with tmpl.

grails-app/views/hello/_msg.gson

model {
    String string
}
json.message {
    hello string
}

grails-app/views/hello/index.gson

model {
    String someString
}
json tmpl.msg(someString)

If a collection is rendered, the property name of the component type is appended with the property name of the collection type. The component type is based on the first item in the list.

List<String> → stringList

Set<String> → stringSet

Bag<String> → stringCollection

If the collection is empty, emptyCollection will be used as the default model name. This is due to not being able to inspect the first object’s type.

grails-app/views/hello/index.gson

model {
    String stringList
}
json {
    strings stringList
}

The variable can be passed in directly to respond.

grails-app/controllers/HelloController.groovy

def index() {
    respond(["Hello", "World"])
}
 The component+collection convention does not apply when rendering collections with tmpl inside a view.

Template Name

When using a template, unless specified, the model name is based on the template name.

Given the following template:

grails-app/views/hello/_msg.gson

model {
    String msg // or String string
}
json.message {
    hello msg
}

To render a single message from another view using the template:

grails-app/views/hello/index.gson

json.message tmpl.msg(message)

To render a collection of messages from another view using the template:

grails-app/views/hello/index.gson

model {
    List<String> stringList
}
json {
    messages tmpl.msg(stringList)
}

In both cases the convention of the variable name matching the template name is used.

2.8. Content Negotiation

GSON views integrate with Grails' content negotiation infrastructure. For example if you create two views called grails-app/views/book/show/show.gsp (for HTML) and grails-app/views/book/show/show.gson (for JSON), you can then define the following action:

grails-app/controllers/myapp/BookController.groovy

def show() {
    respond Book.get(params.id)
}

The result is that if you send a request to /book/show it will render show.gsp but if you send a request to /book/show.json it will render show.gson.

In addition, if the client sends a request with the Accept header containing application/json the show.gson view will be rendered.

2.8.1. Content Negotiation and Domain Classes

Content negotiation also works nicely with domain classes, for example if you want to define a template to render any instance of the Book domain class you can create a gson file that matches the class name.

For example given a class called demo.Book you can create grails-app/views/book/_book.gson and whenever respond is called with an instance of Book Grails will render _book.gson.

def show() {
    respond Book.get(params.id)
}

If you define an index action that responds with a list of books:

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

Then you can create a corresponding grails-app/views/book/index.gson file that renders each book:

grails-app/views/book/index.gson

@Field List<Book> bookList

json tmpl.book(bookList)
 When responding with a list of objects Grails automatically appends the suffix "List" to the model name, so in this case the model name is bookList

By calling the tmpl.book(..) method with the list of books the grails-app/views/book/_book.gson template will be rendered for each one and a JSON array returned.

2.8.2. Global Default Template

You can also define a /object/_object template that is rendered by default if no other template is found during content negotiation. To do this create a file called /grails-app/views/object/_object.gson where the name of the model is object, for example:

model {
    Object object
}
json g.render(object)

2.8.3. Content Negotiation and Versioned APIs

A typical use case when building REST APIs is the requirement to support different versions of the API. GSON views can be versioned by including the version in the name of the view.

Grails will then use the ACCEPT-VERSION header when resolving the view.

For example given a view called /book/show.gson if you wish to deprecate your previous API and create a version 2.0 API, you can rename the previous view /book/show_v1.0.gson and create a new /book/show.gson representing the new version of the API.

Then when the client sends a request with the ACCEPT-VERSION header containing v1.0 the /book/show_v1.0.gsonview will be rendered instead of /book/show.gson.

2.8.4. Content Negotiation and View Resolving Strategy

Grails takes into account a number of factors when attempting to resolve the view including the content type, version and locale.

The following paths are searched:

  • view_name[_LOCALE][_ACCEPT_CONTENT_TYPE][_ACCEPT-VERSION].gson (Example: show_de_hal_v1.0.gson)

  • view_name[_LOCALE][_ACCEPT_CONTENT_TYPE].gson (Example: show_de_hal.gson)

  • view_name[_LOCALE][_ACCEPT-VERSION].gson (Example: show_de_v1.0.gson)

  • view_name[_LOCALE].gson (Example: show_de.gson)

  • view_name[_ACCEPT_CONTENT_TYPE][_ACCEPT-VERSION].gson (Example: show_hal_v1.0.gson)

  • view_name[_ACCEPT-VERSION][_ACCEPT_CONTENT_TYPE].gson (Example: show_v1.0_hal.gson)

  • view_name[_ACCEPT_CONTENT_TYPE].gson (Example: show_hal.gson)

  • view_name[_ACCEPT-VERSION].gson (Example: show_v1.0.gson)

  • view_name.gson (Example: show.gson)

The content type (defined by either the ACCEPT header or file extension in the URI) is taken into account to allow different formats for the same view.

2.8.5. Content Negotiation and Custom Mime Types

Some REST APIs use the notion of custom mime types to represent resources. Within Grails you can for example define custom mime types in grails-app/conf/application.yml:

grails:
    mime:
        types:
            all:      "*/*"
            book:     "application/vnd.books.org.book+json"
            bookList: "application/vnd.books.org.booklist+json"

Once these custom mime types have been defined you can then define a view such as show.book.gson for that particular mime type.

2.9. HAL Support

HAL is a standard format for representing JSON that has gained traction for its ability to represent links between resources and provide navigable APIs.

The JSON views plugin for Grails provides HAL support out-of-the-box. All JSON views have access to the hal instance which implements HalViewHelper.

For example:

model {
    Book book
}
json {
    hal.links(book)
    hal.embedded {
        author( book.authors.first() ) { Author author ->
            name author.name
        }
    }
    title book.title
}
 The call to hal.links() has to be the first element within the json{} closure.

This produces the HAL output:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/book/show/1",
            "hreflang": "en",
            "type": "application/hal+json"
        }
    },
    "_embedded": {
        "author": {
            "_links": {
                "self": {
                    "href": "http://localhost:8080/author/show/1",
                    "hreflang": "en",
                    "type": "application/hal+json"
                }
            },
            "name": "Stephen King"
        }
    },
    "title": "The Stand"
}

2.9.1. Generating HAL Links

The above example uses the hal.links(..) method to render links for a domain resource and the hal.embedded(..)method to define any embedded objects that form part of the HAL response.

The hal.links(..) method will by default create a link to the resource, but you can define additional links by annotating the domain class with either grails.rest.Linkable or grails.rest.Resource and using the linkmethod on the object:

book.link(rel:"publisher", href:"http://foo.com/books")

The link will then be included in the HAL output.

If you wish to be specific about which links to include you can do so by passing a map of objects to link to:

model {
    Book book
}
json {
    hal.links(self:book, author: book.author)
    ...
}

Alternatively, if you prefer to define the HAL links yourself then you can do so by passing a closure to the hal.linksmethod:

model {
    Book book
}
json {
    hal.links {
        self {
            href '...'
            hreflang 'en'
            type "application/hal+json"
        }
    }
    ...
}

2.9.2. Rendering Domain Classes as HAL

If you prefer to let the plugin handle the rendering of your object you can use the hal.render(..) method:

model {
    Book book
}
json hal.render(book)

The hal.render method works the same as the g.render method, accepting the same arguments, the difference being it will output HAL links for the object via hal.links and also output associations fetched via a join query for hal.embedded.

For example you can also customize the content of the generated HAL with a closure:

model {
    Book book
}
json hal.render(book) {
    pages 1000
}

2.9.3. Embedded Association and HAL

Generally, when using the hal.render(..) method, _embedded associations are only rendered for associations that have been initialized and eagerly fetched. This means that the following query will not render the book.authorsassociation:

Book.get(params.id)

However, this query will render the book.authors association:

Book.get(params.id, [fetch:[authors:'eager']])

This is by design and to prevent unexpected performance degradation due to N+1 query loading. If you wish to force the render method to render _embedded associations for HAL you can do see using the deep argument:

json hal.render(book, [deep:true])

You can prevent HAL _embedded associations from being rendering using the embedded:false parameter:

model {
    Book book
}
json hal.render(book, [embedded:false])

You can also render embedded associations without using the hal.render(..) method, by using the hal.embedded(..) method:

model {
    Book book
}
json {
    hal.embedded(book)
    title book.title
}
 Like the hal.links(..) method, the hal.embedded(..) method should come first, before any other attributes, in your JSON output

You can also control which associations should be embedded by using a map argument instead:

model {
    Book book
}
json {
    hal.embedded(authors: book.authors)
    title book.title
}

And you can inline the contents of the book without any associations using the hal.inline(..) method:

model {
    Book book
}
json {
    hal.embedded(authors: book.authors)
    hal.inline(book)
}

To customize the contents of the inlined JSON output use a closure:

model {
    Book book
}
json {
    hal.embedded(authors: book.authors)
    hal.inline(book) {
        pages 300
    }
}
 You cannot include additional content after the call to hal.inline(..) as this will produce invalid JSON

You can combine hal.embeddded(..) and hal.links(..) to obtain exactly the links and the embedded content you want:

model {
    Book book
}
json {
    hal.links(self: book )
    hal.embedded(authors: book.authors)
    hal.inline(book) {
        pages 300
    }
}

2.9.4. Specifying the HAL Content Type

The default HAL response content type is application/hal+json, however as discussed in the section on Custom Mime Type you can define your own response content types to represent your resources.

For example given the following configuration in grails-app/conf/application.yml:

grails:
    mime:
        types:
            all:      "*/*"
            book:     "application/vnd.books.org.book+json"

You can set the HAL content type to an explicit content type or one of the named content types defined in grails.mime.types in application.yml:

model {
    Book book
}
hal.type("book")
json {
    ...
}

2.9.5. HAL Pagination

The JSON views plugin for Grails provides navigable pagination support. Like the GSP <g:paginate> tag, the parameters include: totalmaxoffsetsort and order.

For example:

model {
    Iterable<Book> bookList
    Integer bookCount
    Integer max // optional, defaults to 10
    Integer offset // optional, defaults to 0
    String sort // optional
    String order // optional
}
json {
    hal.paginate(Book, bookCount, max, offset, sort, order)
    ...
}
 Similar to hal.links() the hal.paginate() has to be the first element within the json{} closure.

When accessing http://localhost:8080/book?offset=10 this produces the navigable output like:

{
  "_links": {
    "self": {
      "href": "http://localhost:8080/book/index?offset=10&max=10",
      "hreflang": "en_US",
      "type": "application/hal+json"
    },
    "first": {
      "href": "http://localhost:8080/book/index?offset=0&max=10",
      "hreflang": "en_US"
    },
    "prev": {
      "href": "http://localhost:8080/book/index?offset=0&max=10",
      "hreflang": "en_US"
    },
    "next": {
      "href": "http://localhost:8080/book/index?offset=20&max=10",
      "hreflang": "en_US"
    },
    "last": {
      "href": "http://localhost:8080/book/index?offset=40&max=10",
      "hreflang": "en_US"
    }
  },
  ...
}
 If there aren’t enough results to paginate the navigation links will not appear. Likewise the prev and next links are only present when there is a previous or next page.

If you have other links that you want to include along with the pagination links then you can use the hal.links(..)method with pagination arguments:

model {
    Author author
    Iterable<Book> bookList
    Integer bookCount
}
json {
    // the model, type to paginate, and the total count
    hal.links([author:author], Book, bookCount)
    ...
}

2.10. JSON API Support

JSON API is a standard format for representing JSON.

The JSON views plugin for Grails provides JSON API support out-of-the-box. All JSON views have access to the jsonapiinstance which implements JsonApiViewHelper.

Grails views makes a best effort to conform to the JSON API standard, however that only applies to the format of the data. The specification makes many assertions about response codes, url conventions, and other server related concepts that are overreaching.

The JSON API support in Grails also only applies to domain classes. Because the specification relies on concepts of relationships and identifiers, there is no clear way to determine how those concepts would apply to conventional Groovy classes. As a result, normal Groovy classes are not supported.

The simplest example of using JSON API simply passes a domain class to the render method of the jsonapi object.

For example:

model {
    Book book
}
json jsonapi.render(book)

In the example where Book looks like the following:

class Book {
    String title
    Author author
}

This is what an example Book instance rendered with JSON API might look like.

{
  "data": {
    "type": "book",
    "id": "3",
    "attributes": {
      "title": "The Return of the King"
    },
    "relationships": {
      "author": {
        "links": {
          "self": "/author/9"
        },
        "data": {
          "type": "author",
          "id": "9"
        }
      }
    }
  },
  "links": {
    "self": "http://localhost:8080/book/3"
  }
}

2.10.1. JSON API Object

To include the JSON API Object in the response, include a jsonApiObject argument to the render method.

model {
    Book book
}
json jsonapi.render(book, [jsonApiObject: true])

The response will contain "jsonapi":{"version":"1.0"} as the first key in the resulting object.

2.10.2. Meta Object

To add metadata to your response, the meta argument can be passed to render with the value being the object that should be rendered. If this functionality is used in addition to jsonApiObject: true, the metadata will be rendered as the "meta" property of the "jsonapi" object.

model {
    Book book
}
json jsonapi.render(book, [meta: [name: "Metadata Information"]])

The response will contain "meta":{"name":"Metadata Information"} as the first key in the resulting object.

2.10.3. Rendering Errors

The JSON API specification has instructions on how to render errors. In the case of the Grails implementation, this applies to both exceptions and validation errors.

Exceptions

If an exception is passed to the render method, it will be rendered within the specification.

In the example of an exception new RuntimeException("oops!"), the following will be rendered:

{
  "errors": [
    {
      "status": 500,
      "title": "java.lang.RuntimeException",
      "detail": "oops!",
      "source": {
        "stacktrace": [
          //An array of information relating to the stacktrace
        ]
      }
    }
  ]
}

Validation Errors

In the case of validation errors, the response will look like the following when a name property fails the blank constraint:

{
  "errors": [
    {
      "code": "blank",
      "detail": "Property [name] of class [class com.foo.Bar] cannot be blank",
      "source": {
        "object": "com.foo.Bar",
        "field": "name",
        "rejectedValue": "",
        "bindingError": false
      }
    }
  ]
}

2.10.4. Links

In general, links for relationships will be provided when the relationship has a value.

Example output for a has one relationship where the value is null:

"captain": {
  "data": null
}

And when the value is not null:

"author": {
  "links": {
    "self": "/author/9"
  },
  "data": {
    "type": "author",
    "id": "9"
  }
}

Currently links are not supported in the case of to many relationships.

2.10.5. Pagination

The JSON API specification has a section which explains pagination. The Grails implementation follows that specification as it is written.

To enable pagination links in your output you must be rendering a collection and also include some arguments in addition to the collection being rendered. There are two required arguments: total and resource. The default offset is 0 and max is 10.

model {
    List<Book> books
    Integer bookTotal
}
json jsonapi.render(books, [pagination: [total: bookTotal, resource: Book]])

Example links output if bookTotal == 20:

"links": {
  "self": "/books",
  "first": "http://localhost:8080/books?offset=0&max=10",
  "next": "http://localhost:8080/books?offset=10&max=10",
  "last": "http://localhost:8080/books?offset=10&max=10"
}

By default the values for offsetsortmax, and order will come from the parameters with the same names. You can override their values by passing the corresponding argument in the pagination Map.

model {
    List<Book> books
    Integer bookTotal
}
json jsonapi.render(books, [pagination: [total: bookTotal, resource: Book, max: 20, sort: params.myCustomSortArgument]])

2.10.6. Associations

The JSON API specification details how relationships should be rendered. The first way is through a relationships object described here. By default that is now relationships will be rendered in json views.

If you do not wish to render the relationships at all, the associations argument can be passed to render with the value of false.

model {
    Book book
}
json jsonapi.render(book, [associations: false])

The specification also has a section that describes compound documents. If you want one or more of your relationships to be rendered in that manner, you can include the expand argument.

model {
    Book book
}
json jsonapi.render(book, [expand: "author"]) //can also be a list of strings

2.10.7. Includes / Excludes

The JSON API implementation in Grails supports the same includes and excludes support as normal json views. Please see the section on rendering for details.

2.10.8. Identifier Rendering

Grails provides a way to customize the rendering of your domain class identifiers. To override the default behavior, register a bean that implements JsonApiIdRenderStrategy.

grails-app/conf/spring/resources.groovy

beans = {
    jsonApiIdRenderStrategy(MyCustomImplementation)
}

2.11. The JsonTemplateEngine

The JSON Views plugin registers a bean called jsonTemplateEngine of type JsonViewTemplateEngine.

This class is a regular Groovy TemplateEngine and you can use the engine to render JSON views outside the scope of an HTTP request.

For example:

@Autowired
JsonViewTemplateEngine templateEngine
void myMethod() {
        Template t = templateEngine.resolveTemplate('/book/show')
        def writable = t.make(book: new Book(title:"The Stand"))
        def sw = new StringWriter()
        writable.writeTo( sw )
        ...
}

2.12. Static Compilation

JSON views are statically compiled. You can disable static compilation if you prefer by setting grails.views.json.compileStatic:

grails:
    views:
        json:
            compileStatic: false
 If you disable static compilation rendering performance will suffer.

For model variables you need to declare the types otherwise you will get a compilation error:

model {
    Person person
}
json {
    name person.name
    age person.age
}

2.13. Testing

Although generally testing can be done using functional tests via an HTTP client, the JSON views plugin also provides a trait which helps in writing either unit or integration tests.

To use the trait import the grails.plugin.json.view.test.JsonViewTest class and apply it to any Spock or JUnit test:

import grails.plugin.json.view.test.*
import spock.lang.Specification
class MySpec extends Specification implements JsonViewTest {
    ...
}

The trait provides a number of different render method implementations that can either render a JSON view found in grails-app/views or render an inline String. For example to render an inline template:

void "Test render a raw GSON view"() {
    when:"A gson view is rendered"
    JsonRenderResult result = render '''
        model {
            String person
        }
        json.person {
            name person
        }
''', [person:"bob"] 

    then:"The json is correct"
    result.json.person.name == 'bob' 
}
 Use the render method to return a JsonRenderResult passing in a String that is the inline template and a map that is the model
 Assert the parsed JSON, represented by the json property, is correct

To render an existing view or template use named arguments to specify an absolute path to either a template or a view:

when:"A gson view is rendered"
def result = render(template: "/path/to/template", model:[age:10])

then:"The json is correct"
result.json.name == 'Fred'
result.json.age == 10

If you are writing a unit test, and the model involves a domain class, you may need to add the domain class to the mappingContext object in order for it to be rendered correctly:

when:"A gson view is rendered for a domain class"
mappingContext.addPersistentEntity(MyDomain)
def result = render(template: "/path/to/template", model:[myDomain:MyDomain])

then:"The json is correct"
result.json.name == 'Fred'
result.json.age == 10
 Links generated by json views in a unit test may not match what they would normally generate in the standard environment. To fully test links, use a functional test.

2.13.1. New Testing Framework

Since the release of json views, a new testing framework was released for Grails. A new trait has been developed that works with the new testing framework that is designed to replace the existing trait. The existing trait will be left as is for backwards compatibility.

The new trait works exactly the same way as the old one, however since the new trait is designed to work with the new framework, there are several benefits you can take advantage of. The first is configuration. In the old trait the application configuration was not included in the template engine, which had the potential to produce incorrect results. Other benefits include extensibility and features like @OnceBefore.

To get started, add the org.grails:views-json-testing-support:{version} dependency to your project and implement the JsonViewUnitTest trait in your test instead of JsonViewTest.

 The new testing trait, like the testing framework, requires Spock.

2.14. Plugin Support

Grails plugins as well as standard Gradle Java/Groovy projects can provide json views to your application.

2.14.1. Grails Plugins

Since JSON views are compiled all of a plugin’s views and templates are available for use in your applications.

The view resolution will look through all of the application’s configured plugins for views that match a particular name. By default, the views a plugin supplies should be stored in grails-app/views, just like applications.

2.14.2. Basic Libraries

The most common use case to provide views in a standard library is to provide global templates. Global templates are templates designed to render a given class as JSON. In order to provide views in a standard Gradle project, you should configure your own view compilation task.

Below is an example Gradle build that adds a compileViews task for templates located into src/main/gson:

buildscript {
    repositories {
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails.plugins:views-gradle:2.0.0.RC2"
    }
}

import grails.views.gradle.json.*

apply plugin:"java"

repositories {
    maven { url "https://repo.grails.org/grails/core" }
}

dependencies {
    compile "org.grails.plugins:views-json:2.0.0.RC2"
    compileOnly "org.grails:grails-plugin-rest:3.1.7"
    compileOnly "javax.servlet:javax.servlet-api:4.0.1"
}

task( compileViews, type:JsonViewCompilerTask ) {
    source = project.file('src/main/gson')
    destinationDir = project.file('build/classes/main')
    packageName = ""
    classpath = configurations.compile + configurations.compileOnly
}
classes.dependsOn compileViews

Once this is in place any applications that includes this library will have access to the templates provided.

For example if you want to render all instances of type foo.bar.Birthday create a template called src/main/gson/foo/bar/_birthday.gson then compile the template and place the JAR on the classpath of your application.

 See the GeoJSON templates for MongoDB for example of how it provides global templates for Mongo specific classes.

2.14.3. Customizing the Compile Task

Unless otherwise configured, the project name of the plugin (the gradle project.name) is used as the packageNamewhen compiling JSON views.

In Gradle project.name is generally calculated from the current directory. That means that if there is mismatch between the current directory and your plugin name view resolution from a plugin could fail.

For example consider a plugin named FooBarGrailsPlugin, in this case Grails will search for views that match the plugin name foo-bar. However, if the directory where the plugin is located is called fooBar instead of foo-bar then view resolution will fail and the view will not be found when the plugin is installed into an application.

To resolve this issue you can customize the compileGsonViews task in the plugin’s build.gradle

compileGsonViews {
    packageName = "foo-bar"
}

By setting the packageName property to correctly match the convention of the plugin named (FooBarGrailsPluginmaps to foo-bar) view resolution will succeed.

2.15. Configuration

JSON views configuration can be altered within grails-app/conf/application.yml. Any of the properties within the JsonViewConfiguration interface can be set. The json view configuration extends GenericViewConfiguration, therefore any properties in that interface can be set as well.

For example:
grails:
    views:
        json:
            compileStatic: true
            cache: true
            ...

Alternatively you can register a new JsonViewConfiguration bean using the bean name jsonViewConfiguration in grails-app/conf/resources.groovy.

The same settings in grails-app/conf/application.yml will also be used by the Gradle plugin for production compilation.

The Gradle plugin compiles views using a forked compiler. You can configure the forked compilation task in Gradle as follows:

compileGsonViews {
    compileOptions.forkOptions.memoryMaximumSize = '512mb'
}

See the API for GroovyForkOptions for more information.

2.15.1. Changing the view base class

All JSON views subclass the JsonViewTemplate class by default.

You can however change the subclass (which should be a subclass of JsonViewTemplate) using configuration:

grails:
    views:
        json:
            compileStatic: true
            baseTemplateClass: com.example.MyCustomJsonViewTemplate

2.15.2. Adding New Helper Methods via Traits

Alternatively, rather than modifying the base class, you can instead just add new methods via traits.

For example the HttpView uses the Enhances annotation to add the page object to all views:

import grails.artefact.Enhances
import grails.views.Views

@Enhances(Views.TYPE)
trait HttpView {

    /**
     * @return The response object
     */
    Response response
    ...
}

The result is all JSON views have a response object that can be used to control the HTTP response:

response.header "Token", "foo"
 The trait cannot be defined in the same project as you are compilation as it needs to be on the classpath of the project you are compiling. You will need to create a Grails plugin and use a multi-project build in this scenario.

2.15.3. Default Mime Types

The mime types that will be accepted by default for JSON view rendering is configurable.

grails:
    views:
        json:
            mimeTypes:
              - application/json
              - application/hal+json

2.15.4. Unicode Escaping

By default, unicode characters will not be escaped. If you would like unicode characters to be escaped in your output, set the following configuration.

grails:
    views:
        json:
            generator:
                escapeUnicode: true

2.15.5. Default Date Format

The default format for java.util.Date can be configured.

grails:
    views:
        json:
            generator:
                dateFormat: "yyyy-MM-dd'T'HH:mm:ss'Z'"

In addition, the default locale can also be specified.

grails:
    views:
        json:
            generator:
                locale: "en/US"

2.15.6. Default Time Zone

The default time zone can be configured. The value here will be passed to TimeZone.getTimeZone(...)

grails:
    views:
        json:
            generator:
                timeZone: "GMT"

2.16. Custom Converters

It is possible to register custom converters to change how a given class is rendered with json views. To do so, create a class that implements Converter. Then you must register the class in src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter.

package foo

class MyConverter implements JsonGenerator.Converter {

    @Override
    boolean handles(Class<?> type) {
        CustomClass.isAssignableFrom(type)
    }

    @Override
    Object convert(Object value, String key) {
        ((CustomClass)value).name
    }
}


class CustomClass {
    String name
}

Because plugins could potentially provide converters, you can also determine the order by implementing the Orderedinterface in your converter.

src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter

foo.MyCustomConverter
 If you have multiple classes to register, put each one on it’s own line

2.17. IntelliJ Support

When opening .gson files in Intellij they should be opened with the regular Groovy editor.

The plugin includes an IntelliJ GDSL file that provides code completion when using IntelliJ IDEA.

Intellij GDSL is a way to describe the available methods and properties so that code completion is available to the developer when opening .gson files.

The GDSL file is generally kept up-to-date with the codebase, however if any variation is spotted please raise an issue.

2.18. Debugging Views

Generally views should be kept simple and if you arrive to the point where you need to debug a view you probably have too much logic in the view that would be better off handled by the controller prior to supplying the model.

Nevertheless here are a few view debugging tips to help identify problems in the view.

2.18.1. Introspecting the Model

Every JSON view is a Groovy script and like any Groovy script the model is defined in a binding variable. Therefore you can easily find out the model at any time by either logging or printing the binding variables:

model {
    Book book
}
// use the log variable
log.debug "Model is $binding.variables"
// use system out
System.out.println "Model is $binding.variables"
json g.render(book)
 If you using the log variable then you will need to enabling logging for the grails.views package in grails-app/conf/logback.groovy

2.18.2. Connecting a Debugger

Some IDEs like Intellij IDE allow you to set break points and step debug within the view itself. As mentioned previously you shouldn’t arrive to this point and if you do you have too much logic in your view, but if you do need it the feature is there.

 类似资料: