編程的樂趣和挑戰之一,就是將體力活自動化,使效率成十倍百倍的增長。
需求
做一個項目,需要返回一個很大的 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 ,分三種情形處理:
- 值為基本類型: 解析出對應的類型 type 和 字段名 name
- 值為 JSON 串: 需要遞歸處理這個 JSON 串
- 值為 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("")
}
}
構建與表示分離
第一版的程序簡單直接,但總感覺有點粗糙。整個處理混在一起,后續要修改恐怕比較困難。能不能更清晰一些呢 ?
可以考慮將構建與表示分離開。
表示
仔細再看下對象模型,可以歸結出三個要素:
- 一個類是一個類節點 ClassNode,有一個名字空間 namespace (類名);
- 有一系列屬性,每個屬性有屬性名與屬性類型,可稱為葉子節點 LeafNode;
- 有一系列子節點類 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 的實現如下。有幾點值得提一下:
- 策略模式。分離了三種情況(基本類型、Map, List)的處理。當有多重 if-else 語句,且每個分支都有大段代碼達到同一個目標時,就可以考慮策略模式處理了。
- 構建器。將 ClassNode 的構建單獨分離到 ClassNodeBuilder 。
- 組合模式。樹形結構的處理,特別適合組合模式。
- 命名構造。使用命名構造器,從而免寫了一些構造器。
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 處理完畢。
合並兩個值,分三種情況:
-
是基本類型,任取一個值;
-
是列表類型,如果已有的為空,當前的非空,則用當前的替代已有的。因為空列表無法解析出列表里的對象。
-
是對象類型,如果兩個對象大小一樣,任取一個;如果兩個對象的大小不一樣,遞歸合並兩個對象。
代碼如下所示。
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 串中自動生成對應的對象模型,使得這個過程自動化,讓類似事情的效率成倍的增長了。原來可能要花費十幾分鍾甚至一個小時之多,現在不到三秒。
讓效率成倍增長的有效之法就是提升代碼和方案的復用性,自動化手工處理。在日常工作中,是否可以想到辦法,讓手頭事情的處理效率能夠十倍百倍的增長呢 ? 這個想法看似有點瘋狂,實際上,更多的原因是我們很少這么思考過吧。
