09.13 做一次面向對象的體操:將JSON字符串轉換為嵌套對象的一種方法


做一次面向對象的體操:將JSON字符串轉換為嵌套對象的一種方法

能不能把這個JSON串轉成相應的對象,更易於使用呢? 為了方便講解,這裡重複寫下JSON串。

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:854393687

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導

{
"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
@Setter
public class ItemCore {
private String id;
private String num;
private Item item;
private ItemPrice itemPrice;
private List<itempricechangelog> itemPriceChangeLogs;
}
@Getter

@Setter
public class Item {
private String sId;
private String gId;
private String num;
private String orderNo;
private String id;
private String itemCoreId;
}
@Getter
@Setter
public class ItemPrice {
private String itemId;
private String price;
private String id;
}
@Getter
@Setter
public class ItemPriceChangeLog {
private String id;
private String itemId;
private String detail;
}
/<itempricechangelog>

注意到,對象裡的屬性是駝峰式,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;
import com.alibaba.fastjson.JSON;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import zzz.study.datastructure.map.TransferUtil;
import static zzz.study.utils.BeanUtil.map2Bean;
public class MapToObject {
private static final String json = "{\\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"
+ "}";
public static void main(String[] args) {
Order order = transferOrder(json);
System.out.println(JSON.toJSONString(order));
}
public static Order transferOrder(String json) {
return relate(underline2camelForMap(group(json)));
}
/**
* 轉換成 Map[tablename:id => Map["field": value]]
*/
public static Map<string>> group(String json) {
Map<string> map = JSON.parseObject(json);
Map<string>> groupedMaps = new HashMap();
map.forEach(
(keyInJson, value) -> {
TableField tableField = TableField.buildFrom(keyInJson);
String key = tableField.getTablename() + ":" + tableField.getId();
Map<string> mapForKey = groupedMaps.getOrDefault(key, new HashMap<>());
mapForKey.put(tableField.getField(), value);
groupedMaps.put(key, mapForKey);
}
);
return groupedMaps;
}
public static Map<string>> underline2camelForMap(Map<string>> underlined) {
Map<string>> groupedMapsCamel = new HashMap<>();
Set<string> ignoreSets = new HashSet();
underlined.forEach(
(key, mapForKey) -> {
Map<string> keytoCamel = TransferUtil.generalMapProcess(mapForKey, TransferUtil::underlineToCamel, ignoreSets);
groupedMapsCamel.put(key, keytoCamel);
}
);
return groupedMapsCamel;
}
/**

* 將分組後的子map先轉成相應單個對象,再按照某個key值進行關聯
*/
public static Order relate(Map<string>> groupedMaps) {
List<item> items = new ArrayList<>();
List<itemcore> itemCores = new ArrayList<>();
List<itemprice> itemPrices = new ArrayList<>();
List<itempricechangelog> itemPriceChangeLogs = new ArrayList<>();
groupedMaps.forEach(
(key, mapForKey) -> {
if (key.startsWith("item:")) {
items.add(map2Bean(mapForKey, Item.class));
}
else if (key.startsWith("item_core:")) {
itemCores.add(map2Bean(mapForKey, ItemCore.class));
}
else if (key.startsWith("item_price:")) {
itemPrices.add(map2Bean(mapForKey, ItemPrice.class));
}
else if (key.startsWith("item_price_change_log:")) {
itemPriceChangeLogs.add(map2Bean(mapForKey, ItemPriceChangeLog.class));
}
}
);
Map<string>> itemMap = items.stream().collect(Collectors.groupingBy(
Item::getItemCoreId
));
Map<string>> itemPriceMap = itemPrices.stream().collect(Collectors.groupingBy(
ItemPrice::getItemId
));
Map<string>> itemPriceChangeLogMap = itemPriceChangeLogs.stream().collect(Collectors.groupingBy(
ItemPriceChangeLog::getItemId
));
itemCores.forEach(
itemCore -> {
String itemId = itemCore.getId();
itemCore.setItem(itemMap.get(itemId).get(0));
itemCore.setItemPrice(itemPriceMap.get(itemId).get(0));
itemCore.setItemPriceChangeLogs(itemPriceChangeLogMap.get(itemId));
}
);
Order order = new Order();
order.setItemCores(itemCores);
return order;
}
}
@Data
public class TableField {
String tablename;

String field;
String id;
public TableField(String tablename, String field, String id) {
this.tablename = tablename;
this.field = field;
this.id = id;
}
public static TableField buildFrom(String combined) {
String[] parts = combined.split(":");
if (parts != null && parts.length == 3) {
return new TableField(parts[0], parts[1], parts[2]);
}
throw new IllegalArgumentException(combined);
}
}
package zzz.study.utils;
import org.apache.commons.beanutils.BeanUtils;
import java.util.Map;
public class BeanUtil {
public static T map2Bean(Map map, Class c) {
try {
T t = c.newInstance();
BeanUtils.populate(t, map);
return t;
} catch (Exception ex) {
throw new RuntimeException(ex.getCause());
}
}
}
/<string>/<string>/<string>/<itempricechangelog>/<itemprice>/<itemcore>/<item>/<string>/<string>/<string>/<string>/<string>/<string>/<string>/<string>/<string>/<string>

代碼重構

group的實現已經不涉及具體業務。這裡重點說下 relate 實現的優化。在實現中看到了 if-elseif-elseif-else 條件分支語句。是否可以做成配置化呢?

做配置化的關鍵在於:將關聯項表達成配置。看看 relate 的前半段,實際上就是一個套路: 匹配某個前綴 – 轉換為相應的Bean – 加入相應的對象列表。 後半段,需要根據關鍵字段(itemCoreId)來構建對象列表的 Map 方便做關聯。因此,可以提取相應的配置項: (prefix, beanClass, BeanMap, BeanKeyFunc)。這個配置項抽象成 BizObjects , 整體配置構成 objMapping 對象。 在這個基礎上,可以將代碼重構如下:

public static Order relate2(Map<string>> groupedMaps) {
ObjectMapping objectMapping = new ObjectMapping();
objectMapping = objectMapping.FillFrom(groupedMaps);
List<itemcore> finalItemCoreList = objectMapping.buildFinalList();
Order order = new Order();
order.setItemCores(finalItemCoreList);
return order;
}
/<itemcore>/<string>

ObjectMapping.java

package zzz.study.algorithm.object;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static zzz.study.utils.BeanUtil.map2Bean;
public class ObjectMapping {
Map<string> objMapping;
public ObjectMapping() {
objMapping = new HashMap<>();
objMapping.put("item", new BizObjects<item>(Item.class, new HashMap<>(), Item::getItemCoreId));
objMapping.put("item_core", new BizObjects<itemcore>(ItemCore.class, new HashMap<>(), ItemCore::getId));
objMapping.put("item_price", new BizObjects<itemprice>(ItemPrice.class, new HashMap<>(), ItemPrice::getItemId));
objMapping.put("item_price_change_log", new BizObjects<itempricechangelog>(ItemPriceChangeLog.class, new HashMap<>(), ItemPriceChangeLog::getItemId));
}
public ObjectMapping FillFrom(Map<string>> groupedMaps) {
groupedMaps.forEach(
(key, mapForKey) -> {
String prefixOfKey = key.split(":")[0];
BizObjects bizObjects = objMapping.get(prefixOfKey);
bizObjects.add(map2Bean(mapForKey, bizObjects.getObjectClass()));
}
);
return this;
}
public List<itemcore> buildFinalList() {
Map<string>> itemCores = objMapping.get("item_core").getObjects();
List<itemcore> finalItemCoreList = new ArrayList<>();
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);
}
);

return finalItemCoreList;
}
}
/<itemcore>/<string>/<itemcore>/<string>/<itempricechangelog>/<itemprice>/<itemcore>/<item>/<string>

BizObjects.java

package zzz.study.algorithm.object;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class BizObjects {
private Class cls;
private Map> map;
private Function keyFunc;
public BizObjects(Class cls, Map> map, Function keyFunc) {
this.cls = cls;
this.map = (map != null ? map : new HashMap<>());
this.keyFunc = keyFunc;
}
public void add(T t) {
K key = keyFunc.apply(t);
List objs = map.getOrDefault(key, new ArrayList<>());
objs.add(t);
map.put(key, objs);
}
public Class getObjectClass() {
return cls;
}
public List get(K key) {
return map.get(key);
}
public T getSingle(K key) {
return (map != null && map.containsKey(key) && map.get(key).size() > 0) ? map.get(key).get(0) : null;
}
public Map> getObjects() {

return Collections.unmodifiableMap(map);
}
}

新的實現的主要特點在於:

去掉了條件語句;

將轉換為嵌套對象的重要配置與邏輯都集中到 objMapping ;

更加對象化的思維。

去掉了條件語句;

將轉換為嵌套對象的重要配置與邏輯都集中到 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 (final NoSuchMethodException e) {
return; // Skip this property setter
}

當 name = “gId” 時,會獲取不到 descriptor 直接返回。 為什麼獲取不到呢,因為 Item propertyDescriptors 緩存裡的 key是 GId ,而不是 gId !

做一次面向對象的體操:將JSON字符串轉換為嵌套對象的一種方法

為什麼 itemPropertyDescriptors 裡的 key 是 GId 呢? 進一步跟蹤到 propertyDescriptors 的生成,在 Introspector.getTargetPropertyInfo 方法中,是根據屬性的 getter/setter 方法來生成 propertyDescriptor 的 name 的。 最終定位的代碼是 Introspector.decapitalize 方法:

public static String decapitalize(String name) {
if (name == null || name.length() == 0) {
return name;
}
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
Character.isUpperCase(name.charAt(0))){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}

這裡 name 是 getter/setter 方法的第四位開始的字符串。比如 gId 的 setter 方法為 setGId ,那麼 name = GId 。根據這個方法得到的 name = GId ,也就是走到中間那個 if 分支了。 之所以這樣,方法的解釋是這樣的:

This normally means converting the first
* character from upper case to lower case, but in the (unusual) special
* case when there is more than one character and both the first and
* second characters are upper case, we leave it alone.
*
* Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays
* as "URL".

真相大白! 當使用 BeanUtils.populate 將 map 轉為對象時,對象的屬性命名要尤其注意: 第二個字母不能是大寫!

收工!

小結

本文展示了一種方法, 將具有內在關聯性的JSON字符串轉成對應的嵌套對象。 當處理複雜業務關聯的數據時,相比過程式的思維,轉換為對象的視角會更容易處理和使用。


分享到:


相關文章: