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

简介: Java Bean 转 Map 的巨坑,注意了!!!(1)

一、背景


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


调试截图:


1.png


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


2.png


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


调试截图:


3.png


存在和 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 不正确。


4.png


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


5.png


对于无参的以 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 所以属性名存在一样的问题就不足为奇了。


相关文章
|
24天前
|
Java C# Swift
Java Stream中peek和map不为人知的秘密
本文通过一个Java Stream中的示例,探讨了`peek`方法在流式处理中的应用及其潜在问题。首先介绍了`peek`的基本定义与使用,并通过代码展示了其如何在流中对每个元素进行操作而不返回结果。接着讨论了`peek`作为中间操作的懒执行特性,强调了如果没有终端操作则不会执行的问题。文章指出,在某些情况下使用`peek`可能比`map`更简洁,但也需注意其懒执行带来的影响。
Java Stream中peek和map不为人知的秘密
|
2月前
|
存储 安全 Java
java集合框架复习----(4)Map、List、set
这篇文章是Java集合框架的复习总结,重点介绍了Map集合的特点和HashMap的使用,以及Collections工具类的使用示例,同时回顾了List、Set和Map集合的概念和特点,以及Collection工具类的作用。
java集合框架复习----(4)Map、List、set
|
2月前
|
Java
【Java集合类面试二十二】、Map和Set有什么区别?
该CSDN博客文章讨论了Map和Set的区别,但提供的内容摘要并未直接解释这两种集合类型的差异。通常,Map是一种键值对集合,提供通过键快速检索值的能力,而Set是一个不允许重复元素的集合。
|
2月前
|
算法 Java 索引
【Java集合类面试四】、 描述一下Map put的过程
这篇文章详细描述了HashMap中put操作的过程,包括首次扩容、计算索引、插入数据以及链表转红黑树和可能的再次扩容。
【Java集合类面试四】、 描述一下Map put的过程
|
2月前
|
安全 Java API
Java 8 流库的魔法革命:Filter、Map、FlatMap 和 Optional 如何颠覆编程世界!
【8月更文挑战第29天】Java 8 的 Stream API 通过 Filter、Map、FlatMap 和 Optional 等操作,提供了高效、简洁的数据集合处理方式。Filter 用于筛选符合条件的元素;Map 对元素进行转换;FlatMap 将多个流扁平化合并;Optional 安全处理空值。这些操作结合使用,能够显著提升代码的可读性和简洁性,使数据处理更为高效和便捷。
37 0
|
2月前
|
存储 Java 索引
|
2月前
|
安全 Java
【Java集合类面试五】、 如何得到一个线程安全的Map?
如何得到一个线程安全的Map的方法包括:使用Collections工具类将Map包装为线程安全,使用java.util.concurrent包下的ConcurrentHashMap,以及不推荐使用性能较差的Hashtable。
|
2月前
|
安全 Java
【Java集合类面试三】、Map接口有哪些实现类?
这篇文章介绍了Java中Map接口的几种常用实现类:HashMap、LinkedHashMap、TreeMap和ConcurrentHashMap,以及它们适用的不同场景和线程安全性。
|
2月前
|
Java Spring 容器
Java SpringBoot 中,动态执行 bean 对象中的方法
Java SpringBoot 中,动态执行 bean 对象中的方法
35 0
|
2月前
|
Java Spring
Java SpringBoot Bean InitializingBean 项目初始化
Java SpringBoot Bean InitializingBean 项目初始化
41 0
下一篇
无影云桌面