能不能把這個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"}
思路與實現(xiàn)
要解決這個問題,需要有一個清晰的思路。
首先,需要知道應該轉成怎樣的目標對象。
其次,需要找到一種方法,建立從JSON串到目標對象的橋梁。
推斷目標對象
仔細觀察可知,每個 key 都是 tablename:field:id 組成,其中 table:id 相同的可以構成一個對象的數(shù)據(jù); 此外,不同的tablename 對應不同的對象,而這些對象之間可以通過相同的 itemId 關聯(lián)。
根據(jù)對JSON字符串的仔細分析(尤其是字段的關聯(lián)性),可以知道: 目標對象應該類似如下嵌套對象:
@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串里的字段是下劃線,遵循各自領域內的命名慣例。這里需要用到一個函數(shù),將Map的key從下劃線轉成駝峰。這個方法在 《Java實現(xiàn)遞歸將嵌套Map里的字段名由駝峰轉為下劃線》 給出。
明確了目標對象,就成功了 30%。 接下來,需要找到一種方法,從指定字符串轉換到這個對象。
算法設計
由于 JSON 并不是與對象結構對應的嵌套結構。需要先轉成容易處理的Map對象。這里的一種思路是,
STEP1: 將 table:id 相同的字段及值分組聚合,得到 Map[tablename:id, mapForKey[field, value]];
STEP2: 將每個 mapForKey[field, value] 轉成 tablename 對應的單個對象 Item, ItemCore, ItemPrice, ItemPriceChangeLog;
STEP3: 然后根據(jù) itemId 來關聯(lián)這些對象,組成最終對象。
代碼實現(xiàn)
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值進行關聯(lián)
? */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的實現(xiàn)已經不涉及具體業(yè)務。這里重點說下 relate 實現(xiàn)的優(yōu)化。在實現(xiàn)中看到了 if-elseif-elseif-else 條件分支語句。是否可以做成配置化呢?
做配置化的關鍵在于:將關聯(lián)項表達成配置??纯?relate 的前半段,實際上就是一個套路: 匹配某個前綴 – 轉換為相應的Bean – 加入相應的對象列表。 后半段,需要根據(jù)關鍵字段(itemCoreId)來構建對象列表的 Map 方便做關聯(lián)。因此,可以提取相應的配置項: (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);? }}
新的實現(xiàn)的主要特點在于:
去掉了條件語句;
將轉換為嵌套對象的重要配置與邏輯都集中到 objMapping ;
更加對象化的思維。
美中不足的是,大量使用了泛型來提高通用性,同時也犧牲了運行時安全的好處(需要強制類型轉換)。 后半段關聯(lián)對象,還是不夠配置化,暫時沒想到更好的方法。
為什么 BizObjects 里要用 Map 而不用 List 來表示多個對象呢 ? 因為后面需要根據(jù) itemCoreId 來關聯(lián)相應對象。如果用 List , 后續(xù)還要一個單獨的 buildObjMap 操作。這里添加的時候就構建 Map ,將行為集中于 BizObjects 內部管理, 為后續(xù)配置化地關聯(lián)對象留下一個空間。
一個小坑
運行結果會發(fā)現(xiàn),轉換后的 item 對象的屬性 sId, gId 的值為 null 。納尼 ? 這是怎么回事呢?
單步調試,運行后,會發(fā)現(xiàn)在 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 方法中,是根據(jù)屬性的 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 。根據(jù)這個方法得到的 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 轉為對象時,對象的屬性命名要尤其注意: 第二個字母不能是大寫!
收工!
小結
本文展示了一種方法, 將具有內在關聯(lián)性的JSON字符串轉成對應的嵌套對象。 當處理復雜業(yè)務關聯(lián)的數(shù)據(jù)時,相比過程式的思維,轉換為對象的視角會更容易處理和使用。