一、背景

有些业务场景下需要将 Java Bean 转成 Map 再使用。

本以为很简单场景,但是坑很多。

二、那些坑

2.0 测试对象


import lombok.Data;import java.util.Date;@Data
public class MockObject extends  MockParent{private Integer aInteger;private Long aLong;private Double aDouble;private Date aDate;
}

父类


import lombok.Data;@Data
public class MockParent {private Long parent;
}

2.1 JSON 反序列化了类型丢失

2.1.1 问题复现

将 Java Bean 转 Map 最常见的手段就是使用 JSON 框架,如 fastjson 、 gson、jackson 等。
但使用 JSON 将 Java Bean 转 Map 会导致部分数据类型丢失。
如使用 fastjson ,当属性为 Long 类型但数字小于 Integer 最大值时,反序列成 Map 之后,将变为 Integer 类型。

maven 依赖:

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.8</version></dependency>

示例代码:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;import java.util.Date;
import java.util.Map;public class JsonDemo {public static void main(String[] args) {MockObject mockObject = new MockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(new Date());mockObject.setADouble(3.4D);mockObject.setParent(3L);String json = JSON.toJSONString(mockObject);Map<String,Object> map =  JSON.parseObject(json, new TypeReference<Map<String,Object>>(){});System.out.println(map);}
}

结果打印:

{“parent”:3,“ADouble”:3.4,“ALong”:2,“AInteger”:1,“ADate”:1657299916477}

调试截图:


通过 Java Visualizer 插件进行可视化查看:

2.2.2 问题描述

存在两个问题
(1) 通过 fastjson 将 Java Bean 转为 Map ,类型会发生转变。
如 Long 变成 Integer ,Date 变成 Long, Double 变成 Decimal 类型等。
(2)在某些场景下,Map 的 key 并非和属性名完全对应,像是通过 get set 方法“推断”出来的属性名。

2.2 BeanMap 转换属性名错误

2.2.1 commons-beanutils 的 BeanMap

maven 版本:

<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.4</version>
</dependency>

代码示例:

import org.apache.commons.beanutils.BeanMap;
import third.fastjson.MockObject;import java.util.Date;public class BeanUtilsDemo {public static void main(String[] args) {MockObject mockObject = new MockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(new Date());mockObject.setADouble(3.4D);mockObject.setParent(3L);BeanMap beanMap = new BeanMap(mockObject);System.out.println(beanMap);}
}

调试截图:

存在和 cglib 一样的问题,虽然类型没问题但是属性名还是不对。

原因分析:

   /*** Constructs a new <code>BeanMap</code> that operates on the* specified bean.  If the given bean is <code>null</code>, then* this map will be empty.** @param bean  the bean for this map to operate on*/public BeanMap(final Object bean) {this.bean = bean;initialise();}

关键代码:

    private void initialise() {if(getBean() == null) {return;}final Class<? extends Object>  beanClass = getBean().getClass();try {//BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );final BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();if ( propertyDescriptors != null ) {for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {if ( propertyDescriptor != null ) {final String name = propertyDescriptor.getName();final Method readMethod = propertyDescriptor.getReadMethod();final Method writeMethod = propertyDescriptor.getWriteMethod();final Class<? extends Object> aType = propertyDescriptor.getPropertyType();if ( readMethod != null ) {readMethods.put( name, readMethod );}if ( writeMethod != null ) {writeMethods.put( name, writeMethod );}types.put( name, aType );}}}}catch ( final IntrospectionException e ) {logWarn(  e );}}

调试一下就会发现,问题出在 BeanInfo 里面 PropertyDescriptor 的 name 不正确。

经过分析会发现
java.beans.Introspector#getTargetPropertyInfo 方法是字段解析的关键


对于无参的以 get 开头的方法名从 index =3 处截取,如 getALong 截取后为 ALong, 如 getADouble 截取后为 ADouble。

然后去构造 PropertyDescriptor:

/*** Creates <code>PropertyDescriptor</code> for the specified bean* with the specified name and methods to read/write the property value.** @param bean   the type of the target bean* @param base   the base name of the property (the rest of the method name)* @param read   the method used for reading the property value* @param write  the method used for writing the property value* @exception IntrospectionException if an exception occurs during introspection** @since 1.7*/PropertyDescriptor(Class<?> bean, String base, Method read, Method write) throws IntrospectionException {if (bean == null) {throw new IntrospectionException("Target Bean class is null");}setClass0(bean);setName(Introspector.decapitalize(base));setReadMethod(read);setWriteMethod(write);this.baseName = base;}

底层使用 java.beans.Introspector#decapitalize 进行解析:

   /*** Utility method to take a string and convert it to normal Java variable* name capitalization.  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.* <p>* Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays* as "URL".** @param  name The string to be decapitalized.* @return  The decapitalized version of the string.*/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);}

从代码中我们可以看出
(1) 当 name 的长度 > 1,且第一个字符和第二个字符都大写时,直接返回参数作为PropertyDescriptor name。
(2) 否则将 name 转为首字母小写

这种处理本意是为了不让属性为类似 URL 这种缩略词转为 uRL ,结果“误伤”了我们这种场景。

2.2.2 使用 cglib 的 BeanMap

cglib 依赖

<!-- https://mvnrepository.com/artifact/cglib/cglib --><dependency><groupId>cglib</groupId><artifactId>cglib-nodep</artifactId><version>3.2.12</version></dependency>

代码示例:

import net.sf.cglib.beans.BeanMap;
import third.fastjson.MockObject;import java.util.Date;public class BeanMapDemo {public static void main(String[] args) {MockObject mockObject = new MockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(new Date());mockObject.setADouble(3.4D);mockObject.setParent(3L);BeanMap beanMapp = BeanMap.create(mockObject);System.out.println(beanMapp);}
}

结果展示:

我们发现类型对了,但是属性名依然不对。

关键代码:
net.sf.cglib.core.ReflectUtils#getBeanGetters
底层也会用到 java.beans.Introspector#decapitalize 所以属性名存在一样的问题就不足为奇了。

三、解决办法

3.1 解决方案

解决方案有很多,本文提供一个基于 dubbo的解决方案。

maven 依赖:

<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo -->
<dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo</artifactId><version>3.0.9</version>
</dependency>

示例代码:

import org.apache.dubbo.common.utils.PojoUtils;
import third.fastjson.MockObject;import java.util.Date;public class DubboPojoDemo {public static void main(String[] args) {MockObject mockObject = new MockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(new Date());mockObject.setADouble(3.4D);mockObject.setParent(3L);Object generalize = PojoUtils.generalize(mockObject);System.out.println(generalize);}
}

调试效果:

Java Visualizer 效果:

3.2 原理解析

大家可以下载源码来简单研究下。
https://github.com/apache/dubbo

核心代码:
org.apache.dubbo.common.utils.PojoUtils#generalize(java.lang.Object)

public static Object generalize(Object pojo) {eturn generalize(pojo, new IdentityHashMap());
}

关键代码:

// pojo 待转换的对象
// history 缓存 Map,提高性能private static Object generalize(Object pojo, Map<Object, Object> history) {if (pojo == null) {return null;}// 枚举直接返回枚举名if (pojo instanceof Enum<?>) {return ((Enum<?>) pojo).name();}// 枚举数组,返回枚举名数组if (pojo.getClass().isArray() && Enum.class.isAssignableFrom(pojo.getClass().getComponentType())) {int len = Array.getLength(pojo);String[] values = new String[len];for (int i = 0; i < len; i++) {values[i] = ((Enum<?>) Array.get(pojo, i)).name();}return values;}// 基本类型返回 pojo 自身if (ReflectUtils.isPrimitives(pojo.getClass())) {return pojo;}// Class 返回 nameif (pojo instanceof Class) {return ((Class) pojo).getName();}Object o = history.get(pojo);if (o != null) {return o;}history.put(pojo, pojo);// 数组类型,递归if (pojo.getClass().isArray()) {int len = Array.getLength(pojo);Object[] dest = new Object[len];history.put(pojo, dest);for (int i = 0; i < len; i++) {Object obj = Array.get(pojo, i);dest[i] = generalize(obj, history);}return dest;}
// 集合类型递归if (pojo instanceof Collection<?>) {Collection<Object> src = (Collection<Object>) pojo;int len = src.size();Collection<Object> dest = (pojo instanceof List<?>) ? new ArrayList<Object>(len) : new HashSet<Object>(len);history.put(pojo, dest);for (Object obj : src) {dest.add(generalize(obj, history));}return dest;}// Map 类型,直接 对 key 和 value 处理if (pojo instanceof Map<?, ?>) {Map<Object, Object> src = (Map<Object, Object>) pojo;Map<Object, Object> dest = createMap(src);history.put(pojo, dest);for (Map.Entry<Object, Object> obj : src.entrySet()) {dest.put(generalize(obj.getKey(), history), generalize(obj.getValue(), history));}return dest;}Map<String, Object> map = new HashMap<String, Object>();history.put(pojo, map);// 开启生成 class 则写入 pojo 的classif (GENERIC_WITH_CLZ) {map.put("class", pojo.getClass().getName());}// 处理 get 方法 for (Method method : pojo.getClass().getMethods()) {if (ReflectUtils.isBeanPropertyReadMethod(method)) {ReflectUtils.makeAccessible(method);try {map.put(ReflectUtils.getPropertyNameFromBeanReadMethod(method), generalize(method.invoke(pojo), history));} catch (Exception e) {throw new RuntimeException(e.getMessage(), e);}}}// 处理公有属性for (Field field : pojo.getClass().getFields()) {if (ReflectUtils.isPublicInstanceField(field)) {try {Object fieldValue = field.get(pojo);// 对象已经解析过,直接从缓存里读提高性能if (history.containsKey(pojo)) {Object pojoGeneralizedValue = history.get(pojo);// 已经解析过该属性则跳过(如公有属性,且有 get 方法的情况)if (pojoGeneralizedValue instanceof Map&& ((Map) pojoGeneralizedValue).containsKey(field.getName())) {continue;}}if (fieldValue != null) {map.put(field.getName(), generalize(fieldValue, history));}} catch (Exception e) {throw new RuntimeException(e.getMessage(), e);}}}return map;}

关键截图

org.apache.dubbo.common.utils.ReflectUtils#getPropertyNameFromBeanReadMethod

public static String getPropertyNameFromBeanReadMethod(Method method) {if (isBeanPropertyReadMethod(method)) {// get 方法,则从 index =3 的字符小写 + 后面的字符串if (method.getName().startsWith("get")) {return method.getName().substring(3, 4).toLowerCase()+ method.getName().substring(4);}// is 开头方法, index =2 的字符小写 + 后面的字符串if (method.getName().startsWith("is")) {return method.getName().substring(2, 3).toLowerCase()+ method.getName().substring(3);}}return null;}

因此, getALong 方法对应的属性名被解析为 aLong。


同时,这么处理也会存在问题。如当属性名叫 URL 时,转为 Map 后 key 就会被解析成 uRL。

从这里看出,当属性名比较特殊时也很容易出问题,但 dubbo 这个工具类更符合我们的预期。
更多细节,大家可以根据 DEMO 自行调试学习。

如果想严格和属性保持一致,可以使用反射获取属性名和属性值,加缓存机制提升解析的效率。

四、总结

Java Bean 转 Map 的坑很多,最常见的就是类型丢失和属性名解析错误的问题。
大家在使用 JSON 框架和 Java Bean 转 Map 的框架时要特别小心。
平时使用某些框架时,多写一些 DEMO 进行验证,多读源码,多调试,少趟坑。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。

巨坑系列:Java Bean 转 Map 的那些坑相关推荐

  1. Java Bean 转 Map 的巨坑,注意了!!!

    一.背景 有些业务场景下需要将 Java Bean 转成 Map 再使用. 本以为很简单场景,但是坑很多. 二.那些坑 2.0 测试对象 i mport lombok.Data; import jav ...

  2. Java Bean转Map问题总结

    Java Bean转Map问题总结 1. bean转map问题概述 2. bean转map的6种方式 2.1 环境依赖 2.2 实现验证 3. 梳理总结 1. bean转map问题概述 Java Be ...

  3. Java Bean与Map之间相互转化的实现

    目录树 概述 Apache BeanUtils将Bean转Map Apache BeanUtils将Map转Bean 理解BeanUtils将Bean转Map的实现之手写Bean转Map 概述 Apa ...

  4. Java bean转换map

    /** * * @Title: convertMap * @Description: 使用泛型Map转bean * 首先先把传入的实体bean的类型通过反射实例化,获取该bean的所有方法. * 遍历 ...

  5. java bean转map

    一.使用Apache提供的BeanUtils public Map test(Object person) {Map map = BeanUtils.describe(person);return m ...

  6. 二、java项目常用工具类之beancopy,bean和map转换工具类

    项目环境: jdk1.8+spring4.3.12 一.问题描述及试用场景: 在项目规范中,要求类名以DO为尾的类作为数据库层实体bean,类名以MO为尾的类作为系统传输层实体bean,类名以VO为尾 ...

  7. java工具类-bean转map

    工作常常遇到将java的Bean对象转化为Map,或者将Map转为Bean对象. 常见的手段 通过json工具,将Bean转json,再将json转Map 效率低 jdk的反射,获取类的属性,进行转化 ...

  8. 004-protostuff踩坑-java bean新增字段反序列化失败问题

    protostuff 避免 更改 java 对象字段 ,比如新增一个,导致 redis 等缓存 的数据反序列化失败问题?? 问题重现: 我们有个方法 通过 attrKey 查询 List ,同时方法中 ...

  9. Java Bean Validation 详解

    前言 最近维护一个老项目,项目使用最原始的Servlet,项目中充斥着各种类似判空的简单校验,为了减少重复代码,因此需要手动引入 Java 的 Bean Validation. Java Bean V ...

最新文章

  1. 漫步者蓝牙只有一边有声音_漫步者Dreampods新品体验,蓝牙耳机黑科技来了
  2. AWGN和Rayleigh信道下QPSK的误码率分析
  3. 科大星云诗社动态20211201
  4. 3-4:一个简单的HTTP服务器
  5. ubuntu18.04安装opencv4.3.0
  6. 基于python的气象数据分析_基于python的《Hadoop权威指南》一书中气象数据下载和map reduce化数据处理及其......
  7. java能开发winform程序吗_Java进击C#——应用开发之WinForm环境
  8. ContextCapture、EPS、CASS3D房地一体详细操作流程
  9. 利用CSS实现渐变色边框
  10. Pandas、Numpy 性能优化秘籍(全)
  11. 皮卡丘靶场的搭建以及SQL注入攻击(加强版)
  12. 杏雨梨云Win7 x64 x86 WinXP 3in1 豪华装机版 1306
  13. jfinal java搭建_Jfinal 项目搭建
  14. 别找了,这可能是全网最全的鸿蒙(HarmonyOS)刷机指南!
  15. 财富:最伟大的商业书籍
  16. 安卓手机超强的悬浮窗工具
  17. 软件测试专业社会现状,软件测试行业现状如何?是青春饭吗?
  18. Power BI Premium Per User (PPU) 介绍
  19. 海看股份创业板IPO过会:上半年营收近5亿元,称用户规模扩张受限
  20. 使用PL/SQL,如何获得数字的英文拼写呢?

热门文章

  1. 呼叫中心系统电话评价服务功能
  2. vim中使用cscope的方法
  3. jQuery与Ajax 面试题库(长期更新中...)
  4. 2022中国健康展,睡眠健康,生活健康展,药食同源,大健康展
  5. python 升级setuptools_Python深入:setuptools进阶
  6. 【锐捷无线】隐藏SSID配置
  7. 前端H5使用canvas画爱心以及笑脸
  8. ELK之JStorm
  9. SAP Concur是什么?
  10. 年度十大金融投资书籍