一、問(wèn)題描述
在一次上線后,日志中出現(xiàn)空指針的報(bào)錯(cuò),但是報(bào)錯(cuò)代碼位置以及相應(yīng)工具類(lèi)未進(jìn)行過(guò)修改,接下來(lái)進(jìn)一步分析。
以下為報(bào)錯(cuò)堆棧信息:
java.lang.NullPointerException: null at net.sf.cglib.core.ReflectUtils.getMethodInfo(ReflectUtils.java:424) ~[cglib-3.1.jar:?] at net.sf.cglib.beans.BeanCopier$Generator.generateClass(BeanCopier.java:133) ~[cglib-3.1.jar:?] at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25) ~[cglib-3.1.jar:?] at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216) ~[cglib-3.1.jar:?] at net.sf.cglib.beans.BeanCopier$Generator.create(BeanCopier.java:90) ~[cglib-3.1.jar:?] at net.sf.cglib.beans.BeanCopier.create(BeanCopier.java:50) ~[cglib-3.1.jar:?] at ***.CglibBeanCopier.copyProperties(CglibBeanCopier.java:90) ~[***.jar:1.2.0] at ***.CglibBeanCopier.copyProperties(CglibBeanCopier.java:113) ~[***.jar:1.2.0] at ***.CglibBeanCopier.copyPropertiesOfList(CglibBeanCopier.java:123) ~[***.jar:1.2.0] ..省略
?
二、問(wèn)題分析
1.分析鏈路長(zhǎng),直接拋結(jié)論
通過(guò)Lombok提供的功能使得我們不必在對(duì)象中顯式定義get和set方法。并且Lombok提供鏈?zhǔn)?a target="_blank">編程,通過(guò)在對(duì)象頭部加上@Accessors(chain = true)注解,給屬性賦值時(shí),可以寫(xiě)成obj.setA(a).setB(b).setC(c),省去先new再對(duì)屬性逐個(gè)set賦值。使用了該注解,這個(gè)類(lèi)的set方法返回我就不是void而是this對(duì)象本身。
@Accessors(chain = true) public class YourClass { private int a; @Setter public YourClass setA(int a) { this.a = a; return this; } }
而JDK Introspector(它為目標(biāo)JavaBean提供了一種了解原類(lèi)方法、屬性和事件的標(biāo)準(zhǔn)方法)中對(duì)寫(xiě)入方法是有特殊判斷的,截取Introspector.getBeanInfo(beanClass)中一段源碼,只有返回值是void,且方法名以set作為前綴的,才會(huì)被當(dāng)做writeMethod,即寫(xiě)入方法。所以返回值為void且是“set”開(kāi)頭的才是Introspector認(rèn)為的寫(xiě)入方法,一種狹義的定義。
else if (argCount == 1) { if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) { pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null); } else if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) { // Simple setter pd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method); if (throwsException(method, PropertyVetoException.class)) { pd.setConstrained(true); } } }
像BeanCopier依賴(lài)Introspector的writeMethod對(duì)目標(biāo)類(lèi)賦值的工具,在轉(zhuǎn)換使用了@Accessors(chain = true)注解的類(lèi)時(shí),在獲取屬性描述PropertyDescriptor就不會(huì)返回這個(gè)屬性的writeMethod屬性,就相當(dāng)于該類(lèi)的屬性沒(méi)有“寫(xiě)入方法”,這就造成了拷貝對(duì)象過(guò)程中出現(xiàn)空指針問(wèn)題。
2.分析路徑
List mtProcessDtoList = **WaybillProvider.getMtWayBillProcess(**); List mtProcessList = CglibBeanCopier.copyPropertiesOfList(mtProcessDtoList, WaybillProcess.class); if(CollectionUtils.isNotEmpty(mtProcessList)) { waybillProcessList.addAll(mtProcessList); }
(1)通過(guò)報(bào)錯(cuò)信息定位到代碼端,通常情況看到mtProcessDtoList是從服務(wù)中獲取,第一印象認(rèn)為對(duì)象是可能為null,其實(shí)不然,仔細(xì)看堆棧,問(wèn)題還是出在工具類(lèi)里,
“***.CglibBeanCopier.copyProperties”,繼續(xù)看這段代碼是存在判空操作的,造成空指針的還是copyProperties這個(gè)方法。
public static List copyPropertiesOfList(List??> sourceList, Class targetClass) { if (sourceList == null || sourceList.isEmpty()) { return Collections.emptyList(); } List resultList = new ArrayList?>(sourceList.size()); for (Object o : sourceList) { resultList.add(copyProperties(o, targetClass)); } return resultList; }
(2)具體看copyProperties這個(gè)代碼的實(shí)現(xiàn),工具類(lèi)的封裝的底層能力是BeanCopier提供的,從傳參來(lái)看并沒(méi)有我們常見(jiàn)的傳null后對(duì)null進(jìn)行操作引起的空指針,還需要對(duì)BeanCopier的源碼進(jìn)行分析。
public static void copyProperties(Object source, Object target) { if(source == null || target == null) { log.error("對(duì)象屬性COPY時(shí)入?yún)榭?source:{},target:{}",JSON.toJSONString(source), JSON.toJSONString(target)); return; } if(source instanceof List && target instanceof List) { throw new ParamErrorException("請(qǐng)使用[copyProperties(a,b,c)]方法進(jìn)行集合類(lèi)的值拷貝"); } String beanKey = generateKey(source.getClass(), target.getClass()); BeanCopier copier; if (! beanCopierMap.containsKey(beanKey)) { copier = BeanCopier.create(source.getClass(), target.getClass(), false); beanCopierMap.put(beanKey, copier); } else { copier = beanCopierMap.get(beanKey); } copier.copy(source, target, null); }
(3)由于jar是進(jìn)行反編譯的,堆棧里提供的代碼行數(shù)已經(jīng)失真了,直接貼上報(bào)空指針的源碼截圖。
getMethodInfo入?yún)ember是null,從而導(dǎo)致空指針。需要通過(guò)斷點(diǎn)跟蹤運(yùn)行時(shí)的變量值,找到setters數(shù)組中的元素是如何生成的。
(4)target是作為對(duì)象拷貝的目標(biāo)對(duì)象的類(lèi),setters這個(gè)數(shù)組就是通過(guò)反射獲取該目標(biāo)類(lèi)的所有具備讀方法的描述對(duì)象(PropertyDescriptor對(duì)象,可以理解為屬性/方法描述)。這里面方法名有些歧義,不是說(shuō)只返回getter相關(guān)的屬性對(duì)象,返回的是該類(lèi)所有具備讀或?qū)懛椒ǖ膶傩悦枋?,兩個(gè)布爾值的類(lèi)型分別控制校驗(yàn)讀或?qū)憽?/p>
綜上,由于無(wú)法獲取目標(biāo)類(lèi)的writeMethod,從而沒(méi)有辦法找到這個(gè)屬性的寫(xiě)入方法,就沒(méi)有辦法對(duì)目標(biāo)對(duì)象繼續(xù)賦值。
此時(shí)方向就轉(zhuǎn)到了目標(biāo)類(lèi)的實(shí)現(xiàn)上,分析到這里就跟Lombok產(chǎn)生了聯(lián)系。此處確實(shí)被修改過(guò),WaybillProcess類(lèi)增加了@Accessors這個(gè)注解。
@Setter @Getter @Accessors(chain = true) public class WaybillProcess {}
(5)WaybillProcess使用了@Accessors(chain = true)這個(gè)注解,這就回到了開(kāi)頭提到的,在使用了這個(gè)注解后該類(lèi)生成的set方法返回值就不是void而是this,在通過(guò)Introspector獲取屬性描述時(shí)就不會(huì)被認(rèn)定是寫(xiě)入方法,在去掉這個(gè)注解后,writeMethodName就有值了。
三、解決辦法
解決辦法1:刪除該注解,將工程里鏈?zhǔn)絪et改成了常規(guī)的set賦值方式。
解決辦法2:保留該注解,替換對(duì)象拷貝的工具類(lèi),建議使用MapStruct配合Lombok,直接在編譯時(shí)生成get/set方法,更加安全,功能也更加強(qiáng)大。
四、總結(jié)
凡是依賴(lài)JDK Introspector獲取類(lèi)set方法描述的工具類(lèi)、組件都會(huì)受到其寫(xiě)入方法定義導(dǎo)致的一些列問(wèn)題,目前在工程實(shí)踐中遇到了BeanCopier進(jìn)行對(duì)象拷貝、BeanUtils對(duì)屬性進(jìn)行賦值都會(huì)遇到問(wèn)題。所以大家在日常開(kāi)發(fā)過(guò)程中,如果該類(lèi)已經(jīng)被大面積的使用,在使用組件特性時(shí)需要多留意。
對(duì)于對(duì)象拷貝已經(jīng)有很多最佳實(shí)踐了,有相關(guān)的文章大家可以推薦一下。
感謝閱讀!
審核編輯 黃宇
-
指針
+關(guān)注
關(guān)注
1文章
484瀏覽量
71097 -
JDK
+關(guān)注
關(guān)注
0文章
83瀏覽量
16851
發(fā)布評(píng)論請(qǐng)先 登錄
C語(yǔ)言中空指針和野指針的概念及產(chǎn)生原因
如何有效的處理空指針異常
野指針和空指針的兩個(gè)小點(diǎn)
函數(shù)指針為空的問(wèn)題
【設(shè)計(jì)技巧】指針的使用注意事項(xiàng):空指針、指針賦值、void *指針
空指針引用缺陷分類(lèi)假陽(yáng)性識(shí)別方法

Lombok開(kāi)發(fā)插件使用小技巧
重演自己如何掉入Lombok的戲法陷阱
Lombok同時(shí)使用@Data和@Builder的一個(gè)必須要避開(kāi)的巨坑
Java注解及其底層原理解析 1

Java注解及其底層原理解析2

評(píng)論