close

點擊關注公眾號,Java乾貨及時送達


作者:明明如月學長

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

一、背景

有些業務場景下需要將 Java Bean 轉成 Map 再使用。本以為很簡單場景,但是坑很多。

二、那些坑2.0 測試對象importlombok.Data;importjava.util.Date;@DatapublicclassMockObjectextendsMockParent{privateIntegeraInteger;privateLongaLong;privateDoubleaDouble;privateDateaDate;}

父類

importlombok.Data;@DatapublicclassMockParent{privateLongparent;}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>

示例代碼:

importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.TypeReference;importjava.util.Date;importjava.util.Map;publicclassJsonDemo{publicstaticvoidmain(String[]args){MockObjectmockObject=newMockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(newDate());mockObject.setADouble(3.4D);mockObject.setParent(3L);Stringjson=JSON.toJSONString(mockObject);Map<String,Object>map=JSON.parseObject(json,newTypeReference<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>

代碼示例:

importorg.apache.commons.beanutils.BeanMap;importthird.fastjson.MockObject;importjava.util.Date;publicclassBeanUtilsDemo{publicstaticvoidmain(String[]args){MockObjectmockObject=newMockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(newDate());mockObject.setADouble(3.4D);mockObject.setParent(3L);BeanMapbeanMap=newBeanMap(mockObject);System.out.println(beanMap);}}

調試截圖:

存在和 cglib 一樣的問題,雖然類型沒問題但是屬性名還是不對。

原因分析:

/***Constructsanew<code>BeanMap</code>thatoperatesonthe*specifiedbean.Ifthegivenbeanis<code>null</code>,then*thismapwillbeempty.**@parambeanthebeanforthismaptooperateon*/publicBeanMap(finalObjectbean){this.bean=bean;initialise();}

關鍵代碼:

privatevoidinitialise(){if(getBean()==null){return;}finalClass<?extendsObject>beanClass=getBean().getClass();try{//BeanInfobeanInfo=Introspector.getBeanInfo(bean,null);finalBeanInfobeanInfo=Introspector.getBeanInfo(beanClass);finalPropertyDescriptor[]propertyDescriptors=beanInfo.getPropertyDescriptors();if(propertyDescriptors!=null){for(finalPropertyDescriptorpropertyDescriptor:propertyDescriptors){if(propertyDescriptor!=null){finalStringname=propertyDescriptor.getName();finalMethodreadMethod=propertyDescriptor.getReadMethod();finalMethodwriteMethod=propertyDescriptor.getWriteMethod();finalClass<?extendsObject>aType=propertyDescriptor.getPropertyType();if(readMethod!=null){readMethods.put(name,readMethod);}if(writeMethod!=null){writeMethods.put(name,writeMethod);}types.put(name,aType);}}}}catch(finalIntrospectionExceptione){logWarn(e);}}

調試一下就會發現,問題出在 BeanInfo 裡面 PropertyDescriptor 的 name 不正確。

經過分析會發現 java.beans.Introspector#getTargetPropertyInfo 方法是字段解析的關鍵

對於無參的以 get 開頭的方法名從 index =3 處截取,如 getALong 截取後為 ALong, 如 getADouble 截取後為 ADouble。

然後去構造 PropertyDescriptor:

/***Creates<code>PropertyDescriptor</code>forthespecifiedbean*withthespecifiednameandmethodstoread/writethepropertyvalue.**@parambeanthetypeofthetargetbean*@parambasethebasenameoftheproperty(therestofthemethodname)*@paramreadthemethodusedforreadingthepropertyvalue*@paramwritethemethodusedforwritingthepropertyvalue*@exceptionIntrospectionExceptionifanexceptionoccursduringintrospection**@since1.7*/PropertyDescriptor(Class<?>bean,Stringbase,Methodread,Methodwrite)throwsIntrospectionException{if(bean==null){thrownewIntrospectionException("TargetBeanclassisnull");}setClass0(bean);setName(Introspector.decapitalize(base));setReadMethod(read);setWriteMethod(write);this.baseName=base;}

底層使用 java.beans.Introspector#decapitalize 進行解析:

/***UtilitymethodtotakeastringandconvertittonormalJavavariable*namecapitalization.Thisnormallymeansconvertingthefirst*characterfromuppercasetolowercase,butinthe(unusual)special*casewhenthereismorethanonecharacterandboththefirstand*secondcharactersareuppercase,weleaveitalone.*<p>*Thus"FooBah"becomes"fooBah"and"X"becomes"x",but"URL"stays*as"URL".**@paramnameThestringtobedecapitalized.*@returnThedecapitalizedversionofthestring.*/publicstaticStringdecapitalize(Stringname){if(name==null||name.length()==0){returnname;}if(name.length()>1&&Character.isUpperCase(name.charAt(1))&&Character.isUpperCase(name.charAt(0))){returnname;}charchars[]=name.toCharArray();chars[0]=Character.toLowerCase(chars[0]);returnnewString(chars);}

從代碼中我們可以看出 (1) 當 name 的長度 > 1,且第一個字符和第二個字符都大寫時,直接返回參數作為PropertyDescriptor name。(2) 否則將 name 轉為首字母小寫

這種處理本意是為了不讓屬性為類似 URL 這種縮略詞轉為 uRL ,結果「誤傷」了我們這種場景。微信搜索公眾號:Java項目精選,回覆:java 領取資料 。

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>

代碼示例:

importnet.sf.cglib.beans.BeanMap;importthird.fastjson.MockObject;importjava.util.Date;publicclassBeanMapDemo{publicstaticvoidmain(String[]args){MockObjectmockObject=newMockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(newDate());mockObject.setADouble(3.4D);mockObject.setParent(3L);BeanMapbeanMapp=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>

示例代碼:

importorg.apache.dubbo.common.utils.PojoUtils;importthird.fastjson.MockObject;importjava.util.Date;publicclassDubboPojoDemo{publicstaticvoidmain(String[]args){MockObjectmockObject=newMockObject();mockObject.setAInteger(1);mockObject.setALong(2L);mockObject.setADate(newDate());mockObject.setADouble(3.4D);mockObject.setParent(3L);Objectgeneralize=PojoUtils.generalize(mockObject);System.out.println(generalize);}}

調試效果:

Java Visualizer 效果:

3.2 原理解析

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

publicstaticObjectgeneralize(Objectpojo){eturngeneralize(pojo,newIdentityHashMap());}

關鍵代碼:

//pojo待轉換的對象//history緩存Map,提高性能privatestaticObjectgeneralize(Objectpojo,Map<Object,Object>history){if(pojo==null){returnnull;}//枚舉直接返回枚舉名if(pojoinstanceofEnum<?>){return((Enum<?>)pojo).name();}//枚舉數組,返回枚舉名數組if(pojo.getClass().isArray()&&Enum.class.isAssignableFrom(pojo.getClass().getComponentType())){intlen=Array.getLength(pojo);String[]values=newString[len];for(inti=0;i<len;i++){values[i]=((Enum<?>)Array.get(pojo,i)).name();}returnvalues;}//基本類型返回pojo自身if(ReflectUtils.isPrimitives(pojo.getClass())){returnpojo;}//Class返回nameif(pojoinstanceofClass){return((Class)pojo).getName();}Objecto=history.get(pojo);if(o!=null){returno;}history.put(pojo,pojo);//數組類型,遞歸if(pojo.getClass().isArray()){intlen=Array.getLength(pojo);Object[]dest=newObject[len];history.put(pojo,dest);for(inti=0;i<len;i++){Objectobj=Array.get(pojo,i);dest[i]=generalize(obj,history);}returndest;}//集合類型遞歸if(pojoinstanceofCollection<?>){Collection<Object>src=(Collection<Object>)pojo;intlen=src.size();Collection<Object>dest=(pojoinstanceofList<?>)?newArrayList<Object>(len):newHashSet<Object>(len);history.put(pojo,dest);for(Objectobj:src){dest.add(generalize(obj,history));}returndest;}//Map類型,直接對key和value處理if(pojoinstanceofMap<?,?>){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));}returndest;}Map<String,Object>map=newHashMap<String,Object>();history.put(pojo,map);//開啟生成class則寫入pojo的classif(GENERIC_WITH_CLZ){map.put("class",pojo.getClass().getName());}//處理get方法for(Methodmethod:pojo.getClass().getMethods()){if(ReflectUtils.isBeanPropertyReadMethod(method)){ReflectUtils.makeAccessible(method);try{map.put(ReflectUtils.getPropertyNameFromBeanReadMethod(method),generalize(method.invoke(pojo),history));}catch(Exceptione){thrownewRuntimeException(e.getMessage(),e);}}}//處理公有屬性for(Fieldfield:pojo.getClass().getFields()){if(ReflectUtils.isPublicInstanceField(field)){try{ObjectfieldValue=field.get(pojo);//對象已經解析過,直接從緩存里讀提高性能if(history.containsKey(pojo)){ObjectpojoGeneralizedValue=history.get(pojo);//已經解析過該屬性則跳過(如公有屬性,且有get方法的情況)if(pojoGeneralizedValueinstanceofMap&&((Map)pojoGeneralizedValue).containsKey(field.getName())){continue;}}if(fieldValue!=null){map.put(field.getName(),generalize(fieldValue,history));}}catch(Exceptione){thrownewRuntimeException(e.getMessage(),e);}}}returnmap;}

關鍵截圖

org.apache.dubbo.common.utils.ReflectUtils#getPropertyNameFromBeanReadMethodpublicstaticStringgetPropertyNameFromBeanReadMethod(Methodmethod){if(isBeanPropertyReadMethod(method)){//get方法,則從index=3的字符小寫+後面的字符串if(method.getName().startsWith("get")){returnmethod.getName().substring(3,4).toLowerCase()+method.getName().substring(4);}//is開頭方法,index=2的字符小寫+後面的字符串if(method.getName().startsWith("is")){returnmethod.getName().substring(2,3).toLowerCase()+method.getName().substring(3);}}returnnull;}

因此, getALong 方法對應的屬性名被解析為 aLong。

同時,這麼處理也會存在問題。如當屬性名叫 URL 時,轉為 Map 後 key 就會被解析成 uRL。

從這裡看出,當屬性名比較特殊時也很容易出問題,但 dubbo 這個工具類更符合我們的預期。更多細節,大家可以根據 DEMO 自行調試學習。

如果想嚴格和屬性保持一致,可以使用反射獲取屬性名和屬性值,加緩存機制提升解析的效率。

四、總結

Java Bean 轉 Map 的坑很多,最常見的就是類型丟失和屬性名解析錯誤的問題。大家在使用 JSON 框架和 Java Bean 轉 Map 的框架時要特別小心。平時使用某些框架時,多寫一些 DEMO 進行驗證,多讀源碼,多調試,少趟坑。

點分享

點收藏

點點讚

點在看

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()