Java Bean 转 Map 的巨坑,注意了!!!(bean 转换为map)

  本篇文章为你整理了Java Bean 转 Map 的巨坑,注意了!!!(bean 转换为map)的详细内容,包含有java bean dao bean 转换为map javabean转换成map java bean转json Java Bean 转 Map 的巨坑,注意了!!!,希望能帮助你了解 Java Bean 转 Map 的巨坑,注意了!!!。

  分享Java技术,高并发编程,分布式技术,架构设计,Java面试题,算法,行业动态,程序人生等。

  
作者:明明如月学长
 

  链接:https://juejin.cn/post/7118073840999071751

  有些业务场景下需要将 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);

  

 

  结果打印:

  调试截图:

   通过 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 原理解析

  核心代码: 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 返回 name

   if (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 的class

   if (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 进行验证,多读源码,多调试,少趟坑。

  近期热文推荐:

  1.1,000+ 道 Java面试题及答案整理(2022最新版)

  2.劲爆!Java 协程要来了。。。

  3.Spring Boot 2.x 教程,太全了!

  4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

  5.《Java开发手册(嵩山版)》最新发布,速速下载!

  觉得不错,别忘了随手点赞+转发哦!

  以上就是Java Bean 转 Map 的巨坑,注意了!!!(bean 转换为map)的详细内容,想要了解更多 Java Bean 转 Map 的巨坑,注意了!!!的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: