Java Bean转Map的那些踩坑实战

作者:明明如月学长 时间:2022-12-26 05:42:23 

一、背景

有些业务场景下需要将 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 Bean转Map的那些踩坑实战

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

Java Bean转Map的那些踩坑实战

2.2.2 问题描述

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

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

调试截图:

Java Bean转Map的那些踩坑实战

存在和 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 Bean转Map的那些踩坑实战

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

Java Bean转Map的那些踩坑实战

对于无参的以 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 ,结果&ldquo;误伤&rdquo;了我们这种场景。

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

结果展示:

Java Bean转Map的那些踩坑实战

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

关键代码: 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 Bean转Map的那些踩坑实战

Java Visualizer 效果:

Java Bean转Map的那些踩坑实战

3.2 原理解析

大家可以下载源码来简单研究下。 github.com/apache/dubb&hellip;

核心代码: 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;
   }

关键截图

Java Bean转Map的那些踩坑实战

Java Bean转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。

Java Bean转Map的那些踩坑实战

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

Java Bean转Map的那些踩坑实战

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

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

四、总结

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

来源:https://juejin.cn/post/7118073840999071751

标签:java,bean,map
0
投稿

猜你喜欢

  • jdbc中class.forname的作用

    2023-04-13 00:56:18
  • 深入浅析Java中普通代码块、构造代码块与静态代码块

    2023-04-14 15:16:35
  • Java实战之基于swing的QQ邮件收发功能实现

    2023-11-15 01:34:26
  • C#枚举类型与位域枚举Enum

    2023-03-02 06:52:27
  • Java中重载与重写的对比与区别

    2021-08-29 13:22:31
  • Unity3D选择本地图片并加载

    2023-11-20 09:25:05
  • Java MapStruct解了对象映射的毒

    2022-08-20 11:37:12
  • 解析Java和Eclipse中加载本地库(.dll文件)的详细说明

    2023-11-11 01:23:31
  • Java任意长度byte数组转换为int数组的方法

    2023-02-15 15:26:32
  • 解析C#设计模式编程中外观模式Facade Pattern的应用

    2021-08-08 05:10:37
  • 如何用Spring发送电子邮件

    2023-12-05 07:34:14
  • 详解如何使用Android Studio开发Gradle插件

    2023-11-20 23:39:05
  • C++普通函数指针与成员函数指针实例解析

    2022-09-29 10:19:36
  • c#如何使用 XML 文档功能

    2023-12-25 03:44:33
  • android使用DataBinding来设置空状态

    2022-02-06 22:28:35
  • Java中的CAS和ABA问题说明

    2021-08-19 11:08:18
  • Java中URL传中文时乱码的解决方法

    2022-05-17 02:16:55
  • Spring Boot 多数据源处理事务的思路详解

    2022-04-21 18:21:47
  • C++调用C#的DLL程序实现方法

    2023-02-04 13:58:16
  • 如何在C#9 中使用顶级程序 (top-level)

    2023-08-20 04:13:24
  • asp之家 软件编程 m.aspxhome.com