内容简介:在能不能把这个JSON串转成相应的对象,更易于使用呢? 为了方便讲解,这里重复写下JSON串。要解决这个问题,需要有一个清晰的思路。
背景与问题
在 《一个略复杂的数据映射聚合例子及代码重构》 一文中,将一个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<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;
}
注意到,对象里的属性是驼峰式,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, Map<String,Object>> group(String json) {
Map<String, Object> map = JSON.parseObject(json);
Map<String, Map<String,Object>> groupedMaps = new HashMap();
map.forEach(
(keyInJson, value) -> {
TableField tableField = TableField.buildFrom(keyInJson);
String key = tableField.getTablename() + ":" + tableField.getId();
Map<String,Object> mapForKey = groupedMaps.getOrDefault(key, new HashMap<>());
mapForKey.put(tableField.getField(), value);
groupedMaps.put(key, mapForKey);
}
);
return groupedMaps;
}
public static Map<String, Map<String,Object>> underline2camelForMap(Map<String, Map<String,Object>> underlined) {
Map<String, Map<String,Object>> groupedMapsCamel = new HashMap<>();
Set<String> ignoreSets = new HashSet();
underlined.forEach(
(key, mapForKey) -> {
Map<String,Object> keytoCamel = TransferUtil.generalMapProcess(mapForKey, TransferUtil::underlineToCamel, ignoreSets);
groupedMapsCamel.put(key, keytoCamel);
}
);
return groupedMapsCamel;
}
/**
* 将分组后的子map先转成相应单个对象,再按照某个key值进行关联
*/
public static Order relate(Map<String, Map<String,Object>> 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 ,List<Item>> itemMap = items.stream().collect(Collectors.groupingBy(
Item::getItemCoreId
));
Map<String ,List<ItemPrice>> itemPriceMap = itemPrices.stream().collect(Collectors.groupingBy(
ItemPrice::getItemId
));
Map<String ,List<ItemPriceChangeLog>> 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> T map2Bean(Map map, Class<T> 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<String, Map<String,Object>> groupedMaps) {
ObjectMapping objectMapping = new ObjectMapping();
objectMapping = objectMapping.FillFrom(groupedMaps);
List<ItemCore> 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<String, BizObjects> objMapping;
public ObjectMapping() {
objMapping = new HashMap<>();
objMapping.put("item", new BizObjects<Item,String>(Item.class, new HashMap<>(), Item::getItemCoreId));
objMapping.put("item_core", new BizObjects<ItemCore,String>(ItemCore.class, new HashMap<>(), ItemCore::getId));
objMapping.put("item_price", new BizObjects<ItemPrice,String>(ItemPrice.class, new HashMap<>(), ItemPrice::getItemId));
objMapping.put("item_price_change_log", new BizObjects<ItemPriceChangeLog,String>(ItemPriceChangeLog.class, new HashMap<>(), ItemPriceChangeLog::getItemId));
}
public ObjectMapping FillFrom(Map<String, Map<String,Object>> 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, List<ItemCore>> 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;
}
}
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<T, K> {
private Class<T> cls;
private Map<K, List<T>> map;
private Function<T, K> keyFunc;
public BizObjects(Class<T> cls, Map<K,List<T>> map, Function<T,K> 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<T> objs = map.getOrDefault(key, new ArrayList<>());
objs.add(t);
map.put(key, objs);
}
public Class<T> getObjectClass() {
return cls;
}
public List<T> 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<K, List<T>> 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字符串转成对应的嵌套对象。 当处理复杂业务关联的数据时,相比过程式的思维,转换为对象的视角会更容易处理和使用。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 如何安全地读写深度嵌套的对象?
- JS题目之数组数据拆分重组转成嵌套对象,让脑细胞活跃下
- 支持嵌套对象、多级数组的Vue动态多级表单组件 —— vue-dynamic-form-component
- Python 循环嵌套
- MyBatis嵌套查询解析
- MyBatis嵌套查询解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mastering Regular Expressions, Second Edition
Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95
Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!