能不能把这个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字符串转成对应的嵌套对象。 当处理复杂业务关联的数据时,相比过程式的思维,转换为对象的视角会更容易处理和使用。