歡迎光臨
每天分享高質量文章

做一次面向物件的體操:將 JSON 字串轉換為巢狀物件的一種方法

(點選上方公眾號,可快速關註)


來源:琴水玉 ,

www.cnblogs.com/lovesqcc/p/9478678.html

背景與問題

在 《一個略複雜的資料對映聚合例子及程式碼重構》 一文中,將一個JSON字串轉成了所需要的訂單資訊Map。儘管做了程式碼重構和配置化,過程式的程式碼仍然顯得晦澀難懂,並且客戶端使用Map也非常難受。

https://www.cnblogs.com/lovesqcc/p/7812875.html

能不能把這個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

@Setter

public class ItemCore {

  private String id;

  private String num;

 

  private Item item;

 

  private ItemPrice itemPrice;

 

  private List 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;

}

註意到,物件裡的屬性是駝峰式,JSON串裡的欄位是下劃線,遵循各自領域內的命名慣例。這裡需要用到一個函式,將Map的key從下劃線轉成駝峰。這個方法在 《Java實現遞迴將巢狀Map裡的欄位名由駝峰轉為下劃線》 給出。

https://www.cnblogs.com/lovesqcc/p/6083904.html

明確了標的物件,就成功了 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> group(String json) {

    Map map = JSON.parseObject(json);

    Map> groupedMaps = new HashMap();

    map.forEach(

        (keyInJson, value) -> {

          TableField tableField = TableField.buildFrom(keyInJson);

          String key = tableField.getTablename() + “:” + tableField.getId();

          Map mapForKey = groupedMaps.getOrDefault(key, new HashMap<>());

          mapForKey.put(tableField.getField(), value);

          groupedMaps.put(key, mapForKey);

        }

    );

    return groupedMaps;

  }

 

  public static Map> underline2camelForMap(Map> underlined) {

    Map> groupedMapsCamel = new HashMap<>();

    Set ignoreSets = new HashSet();

    underlined.forEach(

        (key, mapForKey) -> {

          Map keytoCamel = TransferUtil.generalMapProcess(mapForKey, TransferUtil::underlineToCamel, ignoreSets);

          groupedMapsCamel.put(key, keytoCamel);

        }

    );

    return groupedMapsCamel;

  }

 

  /**

   * 將分組後的子map先轉成相應單個物件,再按照某個key值進行關聯

   */

  public static Order relate(Map> groupedMaps) {

    List items = new ArrayList<>();

    List itemCores = new ArrayList<>();

    List itemPrices = new ArrayList<>();

    List 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> 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 -> {

          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());

    }

  }

 

}

程式碼重構

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

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

public static Order relate2(Map> groupedMaps) {

    ObjectMapping objectMapping = new ObjectMapping();

    objectMapping = objectMapping.FillFrom(groupedMaps);

    List finalItemCoreList = objectMapping.buildFinalList();

    Order order = new Order();

    order.setItemCores(finalItemCoreList);

    return order;

  }

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 objMapping;

 

  public ObjectMapping() {

    objMapping = new HashMap<>();

    objMapping.put(“item”, new BizObjects(Item.class, new HashMap<>(), Item::getItemCoreId));

    objMapping.put(“item_core”, new BizObjects(ItemCore.class, new HashMap<>(), ItemCore::getId));

    objMapping.put(“item_price”, new BizObjects(ItemPrice.class, new HashMap<>(), ItemPrice::getItemId));

    objMapping.put(“item_price_change_log”, new BizObjects(ItemPriceChangeLog.class, new HashMap<>(), ItemPriceChangeLog::getItemId));

  }

 

  public ObjectMapping FillFrom(Map> 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 buildFinalList() {

    Map> itemCores = objMapping.get(“item_core”).getObjects();

 

    List 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;

  }

}

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 ;

  • 更加物件化的思維。

美中不足的是,大量使用了泛型來提高通用性,同時也犧牲了執行時安全的好處(需要強制型別轉換)。 後半段關聯物件,還是不夠配置化,暫時沒想到更好的方法。

為什麼 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 !

為什麼 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字串轉成對應的巢狀物件。 當處理複雜業務關聯的資料時,相比過程式的思維,轉換為物件的視角會更容易處理和使用。

【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂