编程的乐趣和挑战之一,就是将体力活自动化,使效率成十倍百倍的增长。

需求

做一个项目,需要返回一个很大的 JSON 串,有很多很多很多字段,有好几层嵌套。前端同学给了一个 JSON 串,需要从这个 JSON 串建立对应的对象模型。

比如,给定 JSON 串:

{"error":0,"status":"success","date":"2014-05-10","extra":{"rain":3,"sunny":2},"recorder":{"name":"qin","time":"2014-05-10 22:00","mood":"good","address":{"provice":"ZJ","city":"nanjing"}},"results":[{"currentCity":"南京","weather_data":[{"date":"周六今天,实时19","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/dayu.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/dayu.png","weather":"大雨","wind":"东南风5-6级","temperature":"18"},{"date":"周日","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/zhenyu.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","weather":"阵雨转多云","wind":"西北风4-5级","temperature":"21~14"}]}]}

解析出对应的对象模型:


@Data
public class Domain implements Serializable {
private Integer error;
private String status;
private String date;
private List<Result> results;
private Extra extra;
private Recorder recorder;
} @Data
public class Extra implements Serializable {
private Integer rain;
private Integer sunny;
} @Data
public class Recorder implements Serializable {
private String name;
private String time;
private String mood;
private Address address;
} @Data
public class Address implements Serializable {
private String provice;
private String city;
} @Data
public class Result implements Serializable {
private String currentCity;
private List<WeatherData> weatherDatas; } @Data
public class WeatherData implements Serializable {
private String date;
private String dayPictureUrl;
private String nightPictureUrl;
private String weather;
private String wind;
private String temperature;
}

怎么办呢 ? 那么复杂的 JSON 串,手写的话,估计得写两个小时吧,又枯燥又容易出错。能否自动生成呢 ?

算法分析

显然,需要遍历这个 JSON ,分三种情形处理:

  1. 值为基本类型: 解析出对应的类型 type 和 字段名 name
  2. 值为 JSON 串: 需要递归处理这个 JSON 串
  3. 值为 List : 简单起见,取第一个元素,如果是基本类型,按基本类型处理,类型为 List[Type] ;如果是 JSON ,则类型为 List[ClassName],然后再递归处理这个 JSON。

一个代码实现

第一版程序如下,简单直接。这里用到了一些知识点:

  • 递归处理: 在 parseMap 方法中实现了递归处理。递归处理需要设置合适的参数结构,当发现又遇到同样的参数结构,就可以递归调用自身。在递归函数中要注意设置条件退出。
  • 函数编程: 在 parseMap 方法中传入 keyConverter 是为了处理下划线转驼峰。不传则默认不转换。
  • JSON 转换为对象:jsonSlurper.parseText(json)
  • 最简单的模板引擎: SimpleTemplateEngine engine.createTemplate(tplText).make(binding).toString()
  • 字符串中的变量引用和方法调用: "${indent()}private ${getType(v)} $k;\n"

JsonParser.groovy

package cc.lovesq.study.json

import groovy.json.JsonSlurper
import static cc.lovesq.study.json.Common.* class JsonParser { def jsonSlurper = new JsonSlurper() def parse(json) {
def obj = jsonSlurper.parseText(json)
Map map = (Map) obj
parseMap(map, 'Domain', Common.&underscoreToCamelCase)
} def parseMap(Map map, String namespace, keyConverter) {
def classTpl = classTpl()
def fields = ""
map.each {
k, v ->
if (!(v instanceof Map) && !(v instanceof List)) {
fields += "${indent()}private ${getType(v)} $k;\n"
}
else { if (v instanceof Map) {
def className = getClsName(k)
fields += "${indent()}private $className $k;\n"
parseMap(v, convert(className, keyConverter), keyConverter)
} if (v instanceof List) {
def obj = v.get(0)
if (!(obj instanceof Map) && !(obj instanceof List)) {
def type = getType(obj)
fields += "${indent()}private List<$type> ${type}s;\n"
}
if (obj instanceof Map) {
def cls = getClsName(k)
if (cls.endsWith('s')) {
cls = cls[0..-2]
}
def convertedClsName = convert(cls,keyConverter)
fields += "${indent()}private List<${convertedClsName}> ${uncapitalize(convertedClsName)}s;\n"
parseMap(obj, convertedClsName, keyConverter)
}
}
}
}
print getString(classTpl, ["Namespace": namespace, "fieldsContent" : fields]) }
}

Common.groovy

package cc.lovesq.study.json

class Common {

    def static getType(v) {
if (v instanceof String) {
return "String"
}
if (v instanceof Integer) {
return "Integer"
}
if (v instanceof Boolean) {
return "Boolean"
}
if (v instanceof Long) {
return "Long"
}
if (v instanceof BigDecimal) {
return "Double"
} "String"
} def static getClsName(String str) {
capitalize(str)
} def static capitalize(String str) {
str[0].toUpperCase() + (str.length() >= 2 ? str[1..-1] : "")
} def static uncapitalize(String str) {
str[0].toLowerCase() + (str.length() >= 2 ? str[1..-1] : "")
} def static classTpl() {
'''
@Data
public class $Namespace implements Serializable {
$fieldsContent
} '''
} def static indent() {
' '
} def static getString(tplText, binding) {
def engine = new groovy.text.SimpleTemplateEngine()
return engine.createTemplate(tplText).make(binding).toString()
} def static convert(key, convertFunc) {
convertFunc == null ? key : convertFunc(key)
} def static underscoreToCamelCase(String underscore){
String[] ss = underscore.split("_")
if(ss.length ==1){
return underscore
} return ss.collect { capitalize(it) }.join("")
}
}

构建与表示分离

第一版的程序简单直接,但总感觉有点粗糙。整个处理混在一起,后续要修改恐怕比较困难。能不能更清晰一些呢 ?

可以考虑将构建与表示分离开。

表示

仔细再看下对象模型,可以归结出三个要素:

  1. 一个类是一个类节点 ClassNode,有一个名字空间 namespace (类名);
  2. 有一系列属性,每个属性有属性名与属性类型,可称为叶子节点 LeafNode;
  3. 有一系列子节点类 ClassNode,子节点类可以递归处理。

实际上,对象模型符合树形结构。如图所示:

节点

可以定义一个节点接口。节点只有一个行为 desc() ,就是描述自己 。 LeafNode 和 ClassNode 分别实现自己的 desc() 。

 interface Node {
String desc()
}

叶子节点

叶子节点比较简单,只有属性名和属性类型。isList 表示是否是 List 类型,比如 List[String]。List 类型的渲染略有不同。


class LeafNode implements Node { String type
String name
Boolean isList = false @Override
String desc() {
isList ? Common.getString("private List<$type> $name;", ["type": type, "name": name]) :
Common.getString("private $type $name;", ["type": type, "name": name])
} }

类节点

类节点包含一系列叶子节点和子节点类(递归特性)。 在描述自身的时候,还要递归描述所包含的子节点类。 这里用到了 findAll {} , collect {} 闭包特性,可以使代码实现更加简洁一些。

class ClassNode implements Node {

    String className = ""
List<LeafNode> leafNodes = []
List<ClassNode> classNodes = []
Boolean isInList = false @Override
String desc() {
def clsTpl = Common.classTpl() def fields = ""
fields += leafNodes.collect { indent() + it.desc() }.join("\n")
def classDef = getString(clsTpl, ["Namespace": className, "fieldsContent" : fields])
if (CollectionUtils.isEmpty(classNodes)) {
return classDef
} fields += "\n" + classNodes.findAll { it.isInList == false }.collect { "${indent()}private ${it.className} ${uncapitalize(it.className)};" }.join("\n")
def resultstr = getString(clsTpl, ["Namespace": className, "fieldsContent" : fields])
resultstr += classNodes.collect { it.desc() }.join("\n")
return resultstr
} boolean addNode(LeafNode node) {
leafNodes.add(node)
true
} boolean addNode(ClassNode classNode) {
classNodes.add(classNode)
true
}
}

这样,就完成了对象模型的表示。

接下来,需要完成 ClassNode 的构建。这个过程与第一版的基本类似,只是从直接打印信息变成了添加节点。

构建

构建 ClassNode 的实现如下。有几点值得提一下:

  1. 策略模式。分离了三种情况(基本类型、Map, List)的处理。当有多重 if-else 语句,且每个分支都有大段代码达到同一个目标时,就可以考虑策略模式处理了。
  2. 构建器。将 ClassNode 的构建单独分离到 ClassNodeBuilder 。
  3. 组合模式。树形结构的处理,特别适合组合模式。
  4. 命名构造。使用命名构造器,从而免写了一些构造器。

ClassNodeBuilder.groovy

package cc.lovesq.study.json

import groovy.json.JsonSlurper

import static cc.lovesq.study.json.Common.*

class ClassNodeBuilder {

    def jsonSlurper = new JsonSlurper()

    def build(json) {
def obj = jsonSlurper.parseText(json)
Map map = (Map) obj
return parseMap(map, 'Domain')
} def static parseMap(Map map, String namespace) {
ClassNode classNode = new ClassNode(className: namespace)
map.each {
k, v ->
getStratgey(v).add(classNode, k, v)
}
classNode
} def static plainStrategy = new AddLeafNodeStrategy()
def static mapStrategy = new AddMapNodeStrategy()
def static listStrategy = new AddListNodeStrategy() def static getStratgey(Object v) {
if (v instanceof Map) {
return mapStrategy
} if (v instanceof List) {
return listStrategy
}
return plainStrategy
} interface AddNodeStrategy {
def add(ClassNode classNode, k, v)
} static class AddLeafNodeStrategy implements AddNodeStrategy { @Override
def add(ClassNode classNode, Object k, Object v) {
classNode.addNode(new LeafNode(type: getType(v), name: k))
}
} static class AddMapNodeStrategy implements AddNodeStrategy { @Override
def add(ClassNode classNode, Object k, Object v) {
v = (Map)v
def className = getClsName(k)
classNode.addNode(parseMap(v, className))
}
} static class AddListNodeStrategy implements AddNodeStrategy { @Override
def add(ClassNode classNode, Object k, Object v) {
v = (List)v
def obj = v.get(0)
if (!(obj instanceof Map) && !(obj instanceof List)) {
def type = getType(obj)
classNode.addNode(new LeafNode(type: "$type", name: "${type}s", isList: true))
}
if (obj instanceof Map) {
def cls = getClsName(underscoreToCamelCase(k))
if (cls.endsWith('s')) {
cls = cls[0..-2]
}
classNode.addNode(new LeafNode(type: "${cls}", name: "${uncapitalize(cls)}s", isList: true)) def subClassNode = parseMap(obj, cls)
subClassNode.isInList = true
classNode.addNode(subClassNode)
}
}
} }

细节优化

文章写成之后,往往要经过一些“润色”。 程序实现之后,也要进行一些细节优化。 有哪些优化点呢 ? 注意到,JSON 数据可能是不可靠的,比如含有 null ,空数组,JSON 中的字段参差不齐等。

健壮性

注意到,添加 List 元素时,有个 get(0) 操作。 如果列表为空会怎样,显然会抛出越界异常了。 因此,需要做判空处理。

字段补全

假设天气数据 weather_datas 是如下所示。

[{},{"date":"周六今天,实时19","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/dayu.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/dayu.png","weather":"大雨","temperature":"18"},{"date":"周日","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/zhenyu.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","wind":"西北风4-5级","temperature":"21~14"}]

取第一个的话,是空对象,这样,WeatherData 对象就解析不出来了;取第二个的话,少了个 wind 字段;取第三个的话,少了个 weather 。无论取哪一个,生成的 WeatherData 都是不完整的。

怎么办呢 ? 此时,就不能取第一个对象,而是要遍历所有的对象,加入所有在 json 中存在的字段。

主要是对 AddListNodeStrategy 进行优化。 如果 JSON 数据是规范的,每个对象都是相同的字段,那么 isTravelFull = false ,取第一个对象解析即可; 如果 JSON 数据是不规范的,则 isTravelFull = true , 需要补全所有的字段。

补全算法:

STEP1:初始化最终的完整对象 full ;

STEP2:遍历所有的对象以及这些对象里的所有 key, value : 如果 full 里没有这个 key ,那么加入这个 key 和 value ;如果 full 里已经有了这个 key, 则取出已有的值 exist 和 当前的值 subv 进行比较和合并,并加入 key 和 合并后的值。 直到所有的对象及对象里的 key , value 处理完毕。

合并两个值,分三种情况:

  1. 是基本类型,任取一个值;

  2. 是列表类型,如果已有的为空,当前的非空,则用当前的替代已有的。因为空列表无法解析出列表里的对象。

  3. 是对象类型,如果两个对象大小一样,任取一个;如果两个对象的大小不一样,递归合并两个对象。

代码如下所示。


static class AddListNodeStrategy implements AddNodeStrategy { /* 是否要遍历列表中的所有元素来拼接成一个完整的对象,适用于 json 的返回数据字段可能有不全的情形 */
private static isTravelFull = true @Override
def add(ClassNode classNode, Object k, Object v) {
v = (List)v
if (CollectionUtils.isEmpty(v)) {
return
}
def obj = v.get(0)
if (!(obj instanceof Map) && !(obj instanceof List)) {
def type = getType(obj)
classNode.addNode(new LeafNode(type: "$type", name: "${type}s", isList: true))
} if (obj instanceof Map) { def cls = getClsName(underscoreToCamelCase(k))
if (cls.endsWith('s')) {
cls = cls[0..-2]
}
classNode.addNode(new LeafNode(type: "${cls}", name: "${uncapitalize(cls)}s", isList: true)) addSubClassNode(classNode, v, cls) } } private void addSubClassNode(ClassNode classNode, v, cls) {
def subObj = v.get(0) if (isTravelFull) {
subObj = mergeToFull(v)
} def subClassNode = parseMap(subObj, cls)
subClassNode.isInList = true
classNode.addNode(subClassNode)
} private Map mergeToFull(List<Map> v) {
Map full = [:]
v.forEach {
map ->
map.forEach {
k, subv ->
if (full.get(k) == null) {
full.put(k, subv)
} else {
def exist = full.get(k)
full.put(k, merge(exist, subv))
}
}
}
return full
} def merge(exist, subv) { // 基本类型 : 取哪个值都一样
if (!exist instanceof List && !exist instanceof Map) {
return exist
} // List : 已有的和当前比较,如果已有的为空列表,当前非空,则用当前替代已有的
if (exist instanceof List && CollectionUtils.isEmpty(exist) && CollectionUtils.isNotEmpty(subv)) {
return subv
} // Map : 两个 map size 不一样, 则 key 一定有不一样的 , 合并两个 map
if (exist instanceof Map && subv != null && exist.size() != (Map)subv.size()) {
return mergeToFull([exist, subv])
}
else {
return exist
}
} }

看来,策略分离还是很有益的。需要做优化或扩展时,只需要有针对性改一处即可。

小结

JSON 是一种具有任意嵌套结构的数据交换格式。要解析 JSON, 通常要用到递归算法。JSON 通常又可以对应到一个对象模型,对象模型可以用树形结构来表示。树形结构同样具有递归特性。

在完成总体设计和实现后 , 往往要进行细节优化。细节的完善决定了实现的完善度。

通过编写程序,从 JSON 串中自动生成对应的对象模型,使得这个过程自动化,让类似事情的效率成倍的增长了。原来可能要花费十几分钟甚至一个小时之多,现在不到三秒。

让效率成倍增长的有效之法就是提升代码和方案的复用性,自动化手工处理。在日常工作中,是否可以想到办法,让手头事情的处理效率能够十倍百倍的增长呢 ? 这个想法看似有点疯狂,实际上,更多的原因是我们很少这么思考过吧。

从JSON中自动生成对应的对象模型的更多相关文章

  1. 使用maven根据JSON文件自动生成Java POJO类(Java Bean)源文件

    根据JSON文件自动生成Java POJO类(Java Bean)源文件 本文介绍使用程序jsonschema2pojo来自动生成Java的POJO类源文件,本文主要使用maven,其他构建工具请参考 ...

  2. 利用在线工具根据JSon数据自动生成对应的Java实体类

    如果你希望根据JSon数据自动生成对应的Java实体类,并且希望能进行变量的重命名,那么“JSON To Java”一定适合你.(下面的地址需要FQ) https://jsontojava.appsp ...

  3. 在 Linux 中自动生成 Cordova/Phonegap for Android 的 APK 安装程序

    在 Linux 中自动生成 Cordova/Phonegap for Android 的 APK 安装程序 本贴首发于: http://xuekaiyuan.com/forum.php?mod=vie ...

  4. IntelliJ IDEA 中自动生成 serialVersionUID 的方法

    as, idea plugin中搜如下关键字,并安装该插件: GenerateSerialVersionUID 如上图所示,创建一个类并实现Serializable接口,然后按alt+Enter键,即 ...

  5. 在PowerDesigner中自动生成sqlserver字段备注

    在PowerDesigner中自动生成sqlserver字段备注 PowerDesigner是数据库设计人员常用的设计工具,但其自生默认生成的代码并不会生成sqlserver数据库的字段备注说明.在生 ...

  6. 二十四、详述 IntelliJ IDEA 中自动生成 serialVersionUID 的方法

    当我们用 IntelliJ IDEA 编写类并实现 Serializable(序列化)接口的时候,可能会遇到这样一个问题,那就是: 无法自动生成serialVersionUID. 而serialVer ...

  7. eclipse中自动生成注释

    eclipse中自动生成注释 包前缀设置的地方 注释模板设置的地方 Eclipse自动生成方法注释 快捷键 自动生成方法的注释格式,例如 /*** @param str* @return* @thro ...

  8. Eclipse中自动生成get/set时携带注释给get/set

    Eclipse中自动生成get/set时携带注释给get/set   编码的时候通常要用到 JavaBean ,而在我们经常把注释写在字段上面,但生成的Get/Set方法不会生成,通过修改Eclips ...

  9. 使用Python从Markdown文档中自动生成标题导航

    概述 知识与思路 代码实现 概述 Markdown 很适合于技术写作,因为技术写作并不需要花哨的排版和内容, 只要内容生动而严谨,文笔朴实而优美. 为了编写对读者更友好的文章,有必要生成文章的标题导航 ...

随机推荐

  1. 在线选题系统完善篇(PHP)

    第一篇: 选题在线提交系统(html+JS+PHP) 这是当时根据需求做的一个简单的版本,只能适用于这一个场景,而且题目等一系列数据都不能改.然后结束后,我又对重新写了一个有后台管理的选题系统.相对于 ...

  2. IP 多播

    IP 多播 一.IP 多播的基本概念 1.1.简介 不使用多播时需要发送 90 次单播: 使用多播时只需要发送 1 次多播: 1.2.IP 多播的一些特点 多播使用组地址:D 类IP地址支持多播.多播 ...

  3. NS域名工作原理及解析

    DNS域名工作原理及解析   0x00 定义 DNS( Domain Name System)是“域名系统”的英文缩写,它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网.D ...

  4. 一起了解 .Net Foundation 项目 No.12

    .Net 基金会中包含有很多优秀的项目,今天就和笔者一起了解一下其中的一些优秀作品吧. 中文介绍 中文介绍内容翻译自英文介绍,主要采用意译.如与原文存在出入,请以原文为准. Cecil Cecil 是 ...

  5. Java中如何更优雅的处理空值

    经常看到项目中存在到处空值判断的情况,这些判断,会让人觉得摸不着头绪,它的出现很有可能和当前的业务逻辑并没有关系.但它会让你很头疼.有时候,更可怕的是系统因为这些空值的情况,会抛出空指针异常,导致业务 ...

  6. 使用VMware12在CentOS7上部署docker实例

    今天下午算是自己搞了一下午才搞出来,对于认为linux是自己死穴的我,现在能搞出来,心里滋味不是一丢丢,哈哈~~~ 算了,废话不多说,直接上图!步骤如下: 1.在安装好VMware12并安装好了cen ...

  7. markdown简明语法1

    目录 Cmd Markdown 简明语法手册 1. 斜体和粗体 2. 分级标题 3. 外链接 4. 无序列表 5. 有序列表 6. 文字引用 7. 行内代码块 8. 代码块 9. 插入图像 Cmd M ...

  8. 2.5D地图系统技术方案

    1.    2.5D地图概述 1.1.    概述 2.5维地图就是根据dem.dom.dlg等数据,以及真三维模型在一定高度.视角和灯光效果,按照轴侧投影的方式生成的地图.本文以臻图信息ZTMapE ...

  9. (28)ASP.NET Core AutoMapper组件

    1.什么是AutoMapper? AutoMapper是一个对象-对象映射器.对象-对象映射通过将一种类型的输入对象转换为另一种类型的输出对象来工作.使AutoMapper变得有趣的是,它提供了一些有 ...

  10. 微信小程序接入LeanCloud

    大家在做小程序或者客户端开发的时候肯定会想使得数据进行联网,但这样就必须有对应的后台服务器以及数据库,再加上linux运维等各种细节,往往会对新手比较劝退,在这里给大家推荐一种bass(后端即服务), ...