jackson json 嵌套对象_做一次面向对象的体操:将JSON字符串转换为嵌套对象的一种方法...

这篇博客介绍了如何使用Jackson库将复杂的JSON字符串转换为Java对象,包括ItemCore、Item、ItemPrice和ItemPriceChangeLog等嵌套对象。通过分组、转换和关联步骤,将JSON数据结构映射到面向对象的表示,提高了代码的可读性和易用性。
摘要由CSDN通过智能技术生成

背景与问题###

在 《一个略复杂的数据映射聚合例子及代码重构》 一文中,将一个JSON字符串转成了所需要的订单信息Map。尽管做了代码重构和配置化,过程式的代码仍然显得晦涩难懂,并且客户端使用Map也非常难受。

能不能把这个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里的字段名由驼峰转为下划线》 给出。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值