Java Bean 轉 Map 的巨坑,註意了!!!

来源:https://www.cnblogs.com/javastack/archive/2022/07/28/16529022.html
-Advertisement-
Play Games

作者:明明如月學長 鏈接:https://juejin.cn/post/7118073840999071751 一、背景 有些業務場景下需要將 Java Bean 轉成 Map 再使用。 本以為很簡單場景,但是坑很多。 二、那些坑 2.0 測試對象 import lombok.Data; impor ...


作者:明明如月學長
鏈接: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);
    }
}

結果列印:

{"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 原理解析

核心代碼: 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開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.認識Spring Security Spring Security提供了聲明式的安全訪問控制解決方案(僅支持基於Spring的應用程式),對訪問許可權進行認證和授權,它基於Spring AOP和Servlet過濾器,提供了安全性方面的全面解決方案。 除常規的認證和授權外,它還提供了 ACLs、LD ...
  • MyDisruptor V6版本介紹 在v5版本的MyDisruptor實現DSL風格的API後。按照計劃,v6版本的MyDisruptor作為最後一個版本,需要對MyDisruptor進行最終的一些細節優化。 v6版本一共做了三處優化: 解決偽共用問題 支持消費者線程優雅停止 生產者序列器中維護消 ...
  • Java數組 9.稀疏數組 什麼是稀疏數組? 當一個數組中大部分元素為0,或者為同一值的數組時,可以使用稀疏數組來保存該數組。 稀疏數組的處理方式是: 記錄數組一共有幾行幾列,有多少個不同的值 把具有不同值 的元素和行列及值記錄在一個小規模的數組中,從而縮小程式的規模 如下圖:左邊是原始數組,右邊是 ...
  • 線程的生命周期 一、通用的java生命周期 ​ 線程的生命周期通常有五種狀態。這五種狀態分別是:新建狀態、就緒狀態、運行狀態、阻塞狀態和死亡狀態。 **新建狀態:**指的是線程已經被創建,但是還不允許分配 CPU 執行。 就緒狀態: 指的是線程可以分配 CPU 執行。在這種狀態下,真正的操作系統線程 ...
  • LVS+KeepAlived高可用部署實戰 1. 構建高可用集群 1.1 什麼是高可用集群 ​ 高可用集群(High Availability Cluster,簡稱HA Cluster),是指以減少服務中斷時間為目的得伺服器集群技術。它通過保護用戶得業務程式對外部間斷提供的服務,把因為軟體,硬體,認 ...
  • 註:本文所用到的版本 MySql 8.0.28 SpringBoot 2.7.2 準備工作 :建表 、pom.xml導入依賴 、application.yml 配置 建表 CREATE TABLE `rna` ( `id` int NOT NULL AUTO_INCREMENT, `name` va ...
  • NoSQL(Not Only SQL),即反SQL運動或者是不僅僅SQL,指的是非關係型的資料庫,是一項全新的資料庫革命運動,是一種全新的思維註入 NoSQL優點 資料庫高併發讀寫 海量數據高效率存儲和訪問 資料庫高擴展性和高可用性 NoSQL缺點 資料庫事務一致性需求 資料庫的寫實時性和讀實時性需 ...
  • 多線程簡介 1.Process與Thread 程式本身是指定和數據的有序集合,其本身沒有任何運行的含義,是一個靜態的概念。 而進程則是執行程式中的一次執行過程,是一個動態的概念。是系統能夠資源分配的單位。 通常在一個進程里,可以包含若幹個線程,當然一個進程至少有一個線程,不然沒有存在的意義。 線程是 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...