能不能把这个JSON串转成相应的对象,更易于使用呢? 为了方便讲解,这里重复写下JSON串。

能不能把这个JSON串转成相应的对象,更易于使用呢? 为了方便讲解,这里重复写下JSON串。

{"item:s_id:18006666":"1024","item:s_id:18008888":"1024","item:g_id:18006666":"6666","item:g_id:18008888":"8888","item:num:18008888":"8","item:num:18006666":"6","item:item_core_id:18006666":"9876666","item:item_core_id:18008888":"9878888","item:order_no:18006666":"E20171013174712025","item:order_no:18008888":"E20171013174712025","item:id:18008888":"18008888","item:id:18006666":"18006666","item_core:num:9878888":"8","item_core:num:9876666":"6","item_core:id:9876666":"9876666","item_core:id:9878888":"9878888","item_price:item_id:1000":"9876666","item_price:item_id:2000":"9878888","item_price:price:1000":"100","item_price:price:2000":"200","item_price:id:2000":"2000","item_price:id:1000":"1000","item_price_change_log:id:1111":"1111","item_price_change_log:id:2222":"2222","item_price_change_log:item_id:1111":"9876666","item_price_change_log:item_id:2222":"9878888","item_price_change_log:detail:1111":"haha1111","item_price_change_log:detail:2222":"haha2222","item_price_change_log:id:3333":"3333","item_price_change_log:id:4444":"4444","item_price_change_log:item_id:3333":"9876666","item_price_change_log:item_id:4444":"9878888","item_price_change_log:detail:3333":"haha3333","item_price_change_log:detail:4444":"haha4444"}

思路与实现

要解决这个问题,需要有一个清晰的思路。

首先,需要知道应该转成怎样的目标对象。

其次,需要找到一种方法,建立从JSON串到目标对象的桥梁。

推断目标对象

仔细观察可知,每个 key 都是 tablename:field:id 组成,其中 table:id 相同的可以构成一个对象的数据; 此外,不同的tablename 对应不同的对象,而这些对象之间可以通过相同的 itemId 关联。

根据对JSON字符串的仔细分析(尤其是字段的关联性),可以知道: 目标对象应该类似如下嵌套对象:

@Getter@SetterpublicclassItemCore{privateString id;privateString num;privateItem item;privateItemPrice itemPrice;privateList itemPriceChangeLogs;}@Getter@SetterpublicclassItem{privateString sId;privateString gId;privateString num;privateString orderNo;privateString id;privateString itemCoreId;}@Getter@SetterpublicclassItemPrice{privateString itemId;privateString price;privateString id;}@Getter@SetterpublicclassItemPriceChangeLog{privateString id;privateString itemId;privateString detail;}

注意到,对象里的属性是驼峰式,JSON串里的字段是下划线,遵循各自领域内的命名惯例。这里需要用到一个函数,将Map的key从下划线转成驼峰。这个方法在 《Java实现递归将嵌套Map里的字段名由驼峰转为下划线》 给出。

明确了目标对象,就成功了 30%。 接下来,需要找到一种方法,从指定字符串转换到这个对象。

算法设计

由于 JSON 并不是与对象结构对应的嵌套结构。需要先转成容易处理的Map对象。这里的一种思路是,

STEP1: 将 table:id 相同的字段及值分组聚合,得到 Map[tablename:id, mapForKey[field, value]];

STEP2: 将每个 mapForKey[field, value] 转成 tablename 对应的单个对象 Item, ItemCore, ItemPrice, ItemPriceChangeLog;

STEP3: 然后根据 itemId 来关联这些对象,组成最终对象。

代码实现

package zzz.study.algorithm.object;importcom.alibaba.fastjson.JSON;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.HashSet;importjava.util.List;importjava.util.Map;importjava.util.Set;importjava.util.stream.Collectors;importzzz.study.datastructure.map.TransferUtil;importstaticzzz.study.utils.BeanUtil.map2Bean;publicclassMapToObject{  privatestaticfinalStringjson ="{\n"+"    \"item:s_id:18006666\": \"1024\",\n"+"    \"item:s_id:18008888\": \"1024\",\n"+"    \"item:g_id:18006666\": \"6666\",\n"+"    \"item:g_id:18008888\": \"8888\",\n"+"    \"item:num:18008888\": \"8\",\n"+"    \"item:num:18006666\": \"6\",\n"+"    \"item:item_core_id:18006666\": \"9876666\",\n"+"    \"item:item_core_id:18008888\": \"9878888\",\n"+"    \"item:order_no:18006666\": \"E20171013174712025\",\n"+"    \"item:order_no:18008888\": \"E20171013174712025\",\n"+"    \"item:id:18008888\": \"18008888\",\n"+"    \"item:id:18006666\": \"18006666\",\n"+"    \n"+"    \"item_core:num:9878888\": \"8\",\n"+"    \"item_core:num:9876666\": \"6\",\n"+"    \"item_core:id:9876666\": \"9876666\",\n"+"    \"item_core:id:9878888\": \"9878888\",\n"+"\n"+"    \"item_price:item_id:1000\": \"9876666\",\n"+"    \"item_price:item_id:2000\": \"9878888\",\n"+"    \"item_price:price:1000\": \"100\",\n"+"    \"item_price:price:2000\": \"200\",\n"+"    \"item_price:id:2000\": \"2000\",\n"+"    \"item_price:id:1000\": \"1000\",\n"+"\n"+"    \"item_price_change_log:id:1111\": \"1111\",\n"+"    \"item_price_change_log:id:2222\": \"2222\",\n"+"    \"item_price_change_log:item_id:1111\": \"9876666\",\n"+"    \"item_price_change_log:item_id:2222\": \"9878888\",\n"+"    \"item_price_change_log:detail:1111\": \"haha1111\",\n"+"    \"item_price_change_log:detail:2222\": \"haha2222\",\n"+"    \"item_price_change_log:id:3333\": \"3333\",\n"+"    \"item_price_change_log:id:4444\": \"4444\",\n"+"    \"item_price_change_log:item_id:3333\": \"9876666\",\n"+"    \"item_price_change_log:item_id:4444\": \"9878888\",\n"+"    \"item_price_change_log:detail:3333\": \"haha3333\",\n"+"    \"item_price_change_log:detail:4444\": \"haha4444\"\n"+"}";  publicstaticvoidmain(String[] args) {    Order order = transferOrder(json);    System.out.println(JSON.toJSONString(order));  }  publicstaticOrder transferOrder(Stringjson) {returnrelate(underline2camelForMap(group(json)));  }/**

  * 转换成 Map[tablename:id => Map["field": value]]

  */publicstaticMap> group(Stringjson) {Map map =JSON.parseObject(json);Map> groupedMaps =newHashMap();    map.forEach(        (keyInJson, value) -> {          TableField tableField = TableField.buildFrom(keyInJson);Stringkey = tableField.getTablename() +":"+ tableField.getId();Map mapForKey = groupedMaps.getOrDefault(key,newHashMap<>());          mapForKey.put(tableField.getField(), value);          groupedMaps.put(key, mapForKey);        }    );returngroupedMaps;  }  publicstaticMap> underline2camelForMap(Map> underlined) {Map> groupedMapsCamel =newHashMap<>();Set ignoreSets =newHashSet();    underlined.forEach(        (key, mapForKey) -> {Map keytoCamel = TransferUtil.generalMapProcess(mapForKey,TransferUtil::underlineToCamel, ignoreSets);          groupedMapsCamel.put(key, keytoCamel);        }    );returngroupedMapsCamel;  }/**

  * 将分组后的子map先转成相应单个对象,再按照某个key值进行关联

  */publicstaticOrder relate(Map> groupedMaps) {    List items =newArrayList<>();    List itemCores =newArrayList<>();    List itemPrices =newArrayList<>();    List itemPriceChangeLogs =newArrayList<>();    groupedMaps.forEach(        (key, mapForKey) -> {if(key.startsWith("item:")) {            items.add(map2Bean(mapForKey, Item.class));          }elseif(key.startsWith("item_core:")) {            itemCores.add(map2Bean(mapForKey, ItemCore.class));          }elseif(key.startsWith("item_price:")) {            itemPrices.add(map2Bean(mapForKey, ItemPrice.class));          }elseif(key.startsWith("item_price_change_log:")) {            itemPriceChangeLogs.add(map2Bean(mapForKey, ItemPriceChangeLog.class));          }        }    );Map> itemMap = items.stream().collect(Collectors.groupingBy(        Item::getItemCoreId    ));Map> itemPriceMap = itemPrices.stream().collect(Collectors.groupingBy(        ItemPrice::getItemId    ));Map> itemPriceChangeLogMap = itemPriceChangeLogs.stream().collect(Collectors.groupingBy(        ItemPriceChangeLog::getItemId    ));    itemCores.forEach(        itemCore -> {StringitemId = itemCore.getId();          itemCore.setItem(itemMap.get(itemId).get(0));          itemCore.setItemPrice(itemPriceMap.get(itemId).get(0));          itemCore.setItemPriceChangeLogs(itemPriceChangeLogMap.get(itemId));        }    );    Order order =newOrder();    order.setItemCores(itemCores);returnorder;  }}

@DatapublicclassTableField{Stringtablename;Stringfield;Stringid;  public TableField(Stringtablename,Stringfield,Stringid) {this.tablename = tablename;this.field = field;this.id = id;  }  publicstaticTableField buildFrom(Stringcombined) {String[] parts = combined.split(":");if(parts !=null&& parts.length ==3) {returnnewTableField(parts[0], parts[1], parts[2]);    }thrownewIllegalArgumentException(combined);  }}

package zzz.study.utils;importorg.apache.commons.beanutils.BeanUtils;importjava.util.Map;publicclassBeanUtil{publicstaticTmap2Bean(Mapmap, Class c){try{      T t = c.newInstance();      BeanUtils.populate(t,map);returnt;    }catch(Exception ex) {thrownewRuntimeException(ex.getCause());    }  }}

代码重构

group的实现已经不涉及具体业务。这里重点说下 relate 实现的优化。在实现中看到了 if-elseif-elseif-else 条件分支语句。是否可以做成配置化呢?

做配置化的关键在于:将关联项表达成配置。看看 relate 的前半段,实际上就是一个套路: 匹配某个前缀 – 转换为相应的Bean – 加入相应的对象列表。 后半段,需要根据关键字段(itemCoreId)来构建对象列表的 Map 方便做关联。因此,可以提取相应的配置项: (prefix, beanClass, BeanMap, BeanKeyFunc)。这个配置项抽象成 BizObjects , 整体配置构成 objMapping 对象。 在这个基础上,可以将代码重构如下:

publicstaticOrder relate2(Map> groupedMaps) {    ObjectMapping objectMapping =newObjectMapping();    objectMapping = objectMapping.FillFrom(groupedMaps);    List finalItemCoreList = objectMapping.buildFinalList();    Order order =newOrder();    order.setItemCores(finalItemCoreList);returnorder;  }

ObjectMapping.java

package zzz.study.algorithm.object;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importstaticzzz.study.utils.BeanUtil.map2Bean;publicclassObjectMapping{Map objMapping;  public ObjectMapping() {    objMapping =newHashMap<>();    objMapping.put("item",newBizObjects(Item.class,newHashMap<>(),Item::getItemCoreId));    objMapping.put("item_core",newBizObjects(ItemCore.class,newHashMap<>(),ItemCore::getId));    objMapping.put("item_price",newBizObjects(ItemPrice.class,newHashMap<>(),ItemPrice::getItemId));    objMapping.put("item_price_change_log",newBizObjects(ItemPriceChangeLog.class,newHashMap<>(),ItemPriceChangeLog::getItemId));  }  public ObjectMapping FillFrom(Map> groupedMaps) {    groupedMaps.forEach(        (key, mapForKey) -> {StringprefixOfKey = key.split(":")[0];          BizObjects bizObjects = objMapping.get(prefixOfKey);          bizObjects.add(map2Bean(mapForKey, bizObjects.getObjectClass()));        }    );returnthis;  }  public List buildFinalList() {Map> itemCores = objMapping.get("item_core").getObjects();    List finalItemCoreList =newArrayList<>();    itemCores.forEach(        (itemCoreId, itemCoreList) -> {          ItemCore itemCore = itemCoreList.get(0);          itemCore.setItem((Item) objMapping.get("item").getSingle(itemCoreId));          itemCore.setItemPrice((ItemPrice) objMapping.get("item_price").getSingle(itemCoreId));          itemCore.setItemPriceChangeLogs(objMapping.get("item_price_change_log").get(itemCoreId));          finalItemCoreList.add(itemCore);        }    );returnfinalItemCoreList;  }}

BizObjects.java

package zzz.study.algorithm.object;importjava.util.ArrayList;importjava.util.Collections;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importjava.util.function.Function;publicclassBizObjects {privateClass cls;privateMap>map;privateFunction keyFunc;publicBizObjects(Class cls, Map>map, Function keyFunc){this.cls = cls;this.map= (map!= null ?map:newHashMap<>());this.keyFunc = keyFunc;  }publicvoidadd(T t){    K key = keyFunc.apply(t);    List objs =map.getOrDefault(key,newArrayList<>());    objs.add(t);map.put(key, objs);  }publicClass getObjectClass() {returncls;  }publicList get(K key) {returnmap.get(key);  }publicTgetSingle(K key){return(map!= null &&map.containsKey(key) &&map.get(key).size() >0) ?map.get(key).get(0) : null;  }publicMap> getObjects() {returnCollections.unmodifiableMap(map);  }}

新的实现的主要特点在于:

去掉了条件语句;

将转换为嵌套对象的重要配置与逻辑都集中到 objMapping ;

更加对象化的思维。

美中不足的是,大量使用了泛型来提高通用性,同时也牺牲了运行时安全的好处(需要强制类型转换)。 后半段关联对象,还是不够配置化,暂时没想到更好的方法。

为什么 BizObjects 里要用 Map 而不用 List 来表示多个对象呢 ? 因为后面需要根据 itemCoreId 来关联相应对象。如果用 List , 后续还要一个单独的 buildObjMap 操作。这里添加的时候就构建 Map ,将行为集中于 BizObjects 内部管理, 为后续配置化地关联对象留下一个空间。

一个小坑

运行结果会发现,转换后的 item 对象的属性 sId, gId 的值为 null 。纳尼 ? 这是怎么回事呢?

单步调试,运行后,会发现在 BeanUtilsBean.java 932 行有这样一行代码(用的是 commons-beanutils 的 1.9.3 版本):

PropertyDescriptor descriptor =null;try{                descriptor =                    getPropertyUtils().getPropertyDescriptor(target, name);if(descriptor ==null) {return;// Skip this property setter}            }catch(finalNoSuchMethodException e) {return;// Skip this property setter}

当 name = “gId” 时,会获取不到 descriptor 直接返回。 为什么获取不到呢,因为 Item propertyDescriptors 缓存里的 key是 GId ,而不是 gId !

为什么 itemPropertyDescriptors 里的 key 是 GId 呢? 进一步跟踪到 propertyDescriptors 的生成,在 Introspector.getTargetPropertyInfo 方法中,是根据属性的 getter/setter 方法来生成 propertyDescriptor 的 name 的。 最终定位的代码是 Introspector.decapitalize 方法:

publicstaticStringdecapitalize(String name){if(name ==null|| name.length() ==0) {returnname;        }if(name.length() >1&& Character.isUpperCase(name.charAt(1)) &&                        Character.isUpperCase(name.charAt(0))){returnname;        }charchars[] = name.toCharArray();        chars[0] = Character.toLowerCase(chars[0]);returnnewString(chars);    }

这里 name 是 getter/setter 方法的第四位开始的字符串。比如 gId 的 setter 方法为 setGId ,那么 name = GId 。根据这个方法得到的 name = GId ,也就是走到中间那个 if 分支了。 之所以这样,方法的解释是这样的:

This normally means converting the first    * character from uppercaseto lowercase, butinthe (unusual) special    *casewhenthere is more than one characterandboth the firstand* second characters are uppercase, we leave it alone.    *      * Thus"FooBah"becomes"fooBah"and"X"becomes"x", but"URL"stays    * as"URL".

真相大白! 当使用 BeanUtils.populate 将 map 转为对象时,对象的属性命名要尤其注意: 第二个字母不能是大写!

收工!

小结

本文展示了一种方法, 将具有内在关联性的JSON字符串转成对应的嵌套对象。 当处理复杂业务关联的数据时,相比过程式的思维,转换为对象的视角会更容易处理和使用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容