使用 JSON Views 技术,让 Controller 返回 JSON 串

简介

当我们实现 REST 接口时,需要让 controller 的方法返回 json 串,这可以用 grails 的 JSON Views 技术来实现。

用gson view的好处是实现 MVC 架构,让视图和其他模块分开;同时可以避免使用复杂的json序列化技术,JSON Views 技术 比常用的json框架(Jackson、Grails的marshaller API、FastJson)更加灵活。例如通过模板的方式可以将公用部分抽取出来重用。

Grails JSON Views 技术和其他的 Grails 技术一样,对同一个实现需求提供了多种实现选择和约定,这对初学者不太友好,其实提供一种方法就足够了,使用约定,如果不熟悉时,会带来额外的困扰及花费更多的学习时间。

因此,我们来总结一个切实可行的 JSON Views 使用方法,在日常开发中尽量都使用这一种方式。

实现步骤分以下几步:

  1. 准备工作,引入 json-view 插件
  2. 在 controller 中指定要渲染的 model 对象
  3. 在 GSON View 中按需求渲染 json 串

第一步,准备工作,引入 json-view 插件

首先需要添加 JSON views 的依赖包

compile 'org.grails.plugins:views-json:2.0.0' // or whatever is the latest version

为了将 JSON views 编译为class,打包到 production 部署文件,我们需要添加 gradle 插件。

buildscript {
    ...
    dependencies {
        ...
        classpath "org.grails.plugins:views-gradle:2.0.0"
    }
}
...
apply plugin: "org.grails.grails-web"
apply plugin: "org.grails.plugins.views-json"

这样会为 gradle 创建一个 compileGsonViews 任务,会在创建 production JAR 或 WAR 文件的时候被预先调用。

第二步,在 controller 中指定要渲染的 model 对象

让 controller 返回一个 json 串的方法是用 respond 函数。respond 函数会根据约定查找对应的视图模板。

约定体现在下面几点:

  • “Content Negotiation”,即 grails 会根据请求头中的 Accept 字段值,例如是 application/json 则返回 json 视图。
  • 模板名称和路径的查找规则,如根据 controller 的方法名查找模板。

使用 respond 指定渲染 model 的例子:

@Secured("ROLE_ADMIN")
class ApiV1Controller {

    StaffService staffService

    /**
     * 列出“坐席”
     */
    def listStaff(){
        TenantUser tenantUser = authenticatedUser as TenantUser
        List<TenantUser> staffList = staffService.findByTenant(tenantUser.tenant)
        respond tenantUserList: staffList
    }
}

这里使用 Map 给 view 传递了一个命名 model tenantUserList: staffList,可以添加更多的 key: value 来传递更多的 model 对象。

第三步,创建一个 JSON View 视图文件

根据约定创建视图文件 views/< controller >/< method >.gson

.gson 文件就是一个普通的 Groovy 脚本文件,它有一个优点,就是可以在 IDEA 中设置断点,查看变量的值。

一个简单的 json view 例子

json.person {
    name "bob"
}

会输出

{"person":{"name":"bob"}}

.gson 文件中的预定义变量 jsonStreamingJsonBuilder 的一个实例。所以我们需要了解 StreamingJsonBuilder 的用法

一个实际的 json view 例子,这个例子中我们需要排除 Domain Class 中的某些属性,让他们不出现在返回的 json 串中。

import com.telecwin.jinanyuan.TenantUser
import groovy.transform.Field

@Field List<TenantUser> tenantUserList

json g.render(tenantUserList, [excludes: ["password"]])

更改 GSON View 文件名、内容后,如果不生效,请重启应用试试。
可能和 GSON view 会编译为 class 的原因有关。

使用 GSON View 模板

json g.render(template:"person", model:[person:person])
# 模板和View不在一个目录下时,使用URI
json g.render(template:"/person/person", model:[person:person])
# tmpl.person 方法名对应了模板名,模板中model变量名和模板名相同
json tmpl.person(person)
# 指定model变量名
json tmpl.person(individual:person)

补充信息

respond 方法

The respond method will then look for an appriopriate Renderer for the object and the calculated media type from the RendererRegistry.

StreamingJsonBuilder 的用法

基本用法

StringWriter writer = new StringWriter()
StreamingJsonBuilder builder = new StreamingJsonBuilder(writer)
builder.records {
  car {
        name 'HSV Maloo'
        make 'Holden'
        year 2006
        country 'Australia'
        record {
            type 'speed'
            description 'production pickup truck with speed of 271kph'
        }
  }
}
String json = JsonOutput.prettyPrint(writer.toString())

会产生下面的 json

    {
        "records": {
           "car": {
	            "name": "HSV Maloo",
	            "make": "Holden",
	            "year": 2006,
	            "country": "Australia",
	            "record": {
	              "type": "speed",
	              "description": "production pickup truck with speed of 271kph"
	             }
           }
      }
    }

定制输出

想要排除Null属性、排除特定属性、自定义对象转换为String的逻辑,可以用 JsonGenerator instance 创建一个 StreamingJsonBuilder ,这样就可以自定义输出逻辑,比如:

def generator = new JsonGenerator.Options()
        .excludeNulls()
        .excludeFieldsByName('make', 'country', 'record')
        .excludeFieldsByType(Number)
        .addConverter(URL) { url -> "http://groovy-lang.org" }
        .build()

StringWriter writer = new StringWriter()
StreamingJsonBuilder builder = new StreamingJsonBuilder(writer, generator)

builder.records {
  car {
        name 'HSV Maloo'
        make 'Holden'
        year 2006
        country 'Australia'
        homepage new URL('http://example.org')
        record {
            type 'speed'
            description 'production pickup truck with speed of 271kph'
        }
  }
}

assert writer.toString() == '{"records":{"car":{"name":"HSV Maloo","homepage":"http://groovy-lang.org"}}}'

去掉顶层多余的对象

用上面方法输出的 json 串,总是有一个多余的对象,如

{
    "records": {
    	"car": {
	            "name": "HSV Maloo",
	     },
	     "ship": {...}
    }
 }

如何去掉 “records” 直接将里面的属性放到顶层,成为下面这样的结构呢?

{
   	"car": {
            "name": "HSV Maloo",
     },
     "ship": {...}
 }

使用 JsonStreamingBuilder 的各种 call() 函数。Groovy 中一个类如果实现了call() 函数,那么实例对象就可以被当成方法一样被调用。
比如 JsonStreamingBuilder 就可以这样来使用:

StreamingJsonBuilder builder = new StreamingJsonBuilder(writer, generator)
builder(myPOJO)

这样就可以将 myPOJO 作为“root JSON object”输出了。

用这种方法不但可以将一个对象作为 root JSON object 输出,还可以把数组、多个对象作为 root JSON array 输出,也可以用来给clousure 传递额外的参数。这些在 StreamingJsonBuilder 的API 中都有说明和举例。

示例代码:

 new StringWriter().with { w ->
   def json = new groovy.json.StreamingJsonBuilder(w)
   def result = json 1, 2, 3

   assert result instanceof List
   assert w.toString() == "[1,2,3]"
 }

更简单的方法是使用 Closure 参数来调用 builder 对象,像这样:

new StringWriter().with { w ->
   def json = new groovy.json.StreamingJsonBuilder(w)
   json {
      name "Tim"
      age 39
   }

   assert w.toString() == '{"name":"Tim","age":39}'
 }

注意:上面指定 json key 的方法是用 函数 调用的方式,也就是说 “name” 是一个函数名,“Tim” 是函数的参数。

但在 GSON views 中如何使用 JsonGenerator 呢?
没有找到,但是可以用

json g.render(book, [excludes:['password'])

来达到排除的目的。回头试试 generator 预定义变量。

参考资料

先不要着急读下面的文档,等看完本blog,熟悉整个使用方法后再来看这些官方文档,否则容易陷入纠结状态

杂记

respond 方法如何指定一个模板?

不用方法名的约定,而是指定使用一个模板,难道非要写一个 json view 文件,在里面使用 tmpl 变量吗?
答:可以指定 view 名。

render 函数的使用方法可以有以下各种。

render(view: "display", model: map)
render(view: "/shared/display", model: map)

class ReportingController {
    static namespace = 'business'
    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]
    }
}

渲染一个text片段可以这样

// render a template for each item in a collection
render(template: 'book_template', collection: Book.list())

使用 GSON view 出现死循环的情况

问题代码如下

import com.telecwin.jinanyuan.api.Response
import groovy.transform.Field

@Field Response response

json {
    code response.code.code
    msg response.code.name()
    info response.info
}

原因:上面的写法info response.info在生成 json string 时,没有将 GORM 添加的额外属性排除在外,导致序列化 “constrainedProperties” 属性,从而进行了很深的对象树序列化工作,造成调用堆栈溢出。

[{"tenantId":1,"constrainedProperties":{"dateCreated":{"editable":true,
...
}]

解决办法:
使用模板,让 grails 知道正在序列化的是一个 GORM 实体对象。

response.gson

import com.telecwin.jinanyuan.api.Response
import groovy.transform.Field

@Field Response response

json {
    code response.code.code
    msg response.code.name()
    info tmpl.tenantUser(response.info)
}

_tenantUser.gson

import com.telecwin.jinanyuan.TenantUser
import groovy.transform.Field

@Field TenantUser tenantUser

json g.render(tenantUser, [excludes: ["password"]]) {
    tenant tenantUser.tenant.id
}

上面的代码还用到了一个技巧,就是“先将 Domain Class 中的属性 tenant 去掉,换成自定义的属性值”,因为原始的 tenant 属性会被渲染成一个对象,而我们希望是一个 int 类型值。

在 GSON View 文件中定义函数、添加判断逻辑

因为 .gson 文件就是一个 Groovy 脚本,所以我们可以各种 groovy 语言的控制语句、函数定义来实现 json 视图逻辑。
下面是一个高级使用例子。

import com.telecwin.jinanyuan.TenantUser
import com.telecwin.jinanyuan.api.Response
import groovy.transform.Field

// 本 gson 对象会被编译为一个 class,这里定义了一个 field
@Field Response response

json {
    code response.code.code
    msg response.code.name()
    if (isTenantUserList(this.response.info)) {
        info tmpl.tenantUser(response.info)
    }else{
        info response.info
    }
}

/**
 * 辅助方法,判断一个对象是否是 List<TenantUser> 类型。
 * @param obj 要判断的对象
 * @return true 是 List<TenantUser> 列表类型,false 不是该类型或者列表的长度为0
 */
static boolean isTenantUserList(def obj) {
    obj instanceof List && obj.size() > 0 && obj[0] instanceof TenantUser
}

发布了63 篇原创文章 · 获赞 25 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yangbo_hr/article/details/105450922