一、問題描述
在一次上線后,日志中出現空指針的報錯,但是報錯代碼位置以及相應工具類未進行過修改,接下來進一步分析。
以下為報錯堆棧信息:
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] ..省略
?
二、問題分析
1.分析鏈路長,直接拋結論
通過Lombok提供的功能使得我們不必在對象中顯式定義get和set方法。并且Lombok提供鏈式編程,通過在對象頭部加上@Accessors(chain = true)注解,給屬性賦值時,可以寫成obj.setA(a).setB(b).setC(c),省去先new再對屬性逐個set賦值。使用了該注解,這個類的set方法返回我就不是void而是this對象本身。
@Accessors(chain = true) public class YourClass { private int a; @Setter public YourClass setA(int a) { this.a = a; return this; } }
而JDK Introspector(它為目標JavaBean提供了一種了解原類方法、屬性和事件的標準方法)中對寫入方法是有特殊判斷的,截取Introspector.getBeanInfo(beanClass)中一段源碼,只有返回值是void,且方法名以set作為前綴的,才會被當做writeMethod,即寫入方法。所以返回值為void且是“set”開頭的才是Introspector認為的寫入方法,一種狹義的定義。
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依賴Introspector的writeMethod對目標類賦值的工具,在轉換使用了@Accessors(chain = true)注解的類時,在獲取屬性描述PropertyDescriptor就不會返回這個屬性的writeMethod屬性,就相當于該類的屬性沒有“寫入方法”,這就造成了拷貝對象過程中出現空指針問題。
2.分析路徑
List mtProcessDtoList = **WaybillProvider.getMtWayBillProcess(**); List mtProcessList = CglibBeanCopier.copyPropertiesOfList(mtProcessDtoList, WaybillProcess.class); if(CollectionUtils.isNotEmpty(mtProcessList)) { waybillProcessList.addAll(mtProcessList); }
(1)通過報錯信息定位到代碼端,通常情況看到mtProcessDtoList是從服務中獲取,第一印象認為對象是可能為null,其實不然,仔細看堆棧,問題還是出在工具類里,
“***.CglibBeanCopier.copyProperties”,繼續看這段代碼是存在判空操作的,造成空指針的還是copyProperties這個方法。
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這個代碼的實現,工具類的封裝的底層能力是BeanCopier提供的,從傳參來看并沒有我們常見的傳null后對null進行操作引起的空指針,還需要對BeanCopier的源碼進行分析。
public static void copyProperties(Object source, Object target) { if(source == null || target == null) { log.error("對象屬性COPY時入參為空,source:{},target:{}",JSON.toJSONString(source), JSON.toJSONString(target)); return; } if(source instanceof List && target instanceof List) { throw new ParamErrorException("請使用[copyProperties(a,b,c)]方法進行集合類的值拷貝"); } 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是進行反編譯的,堆棧里提供的代碼行數已經失真了,直接貼上報空指針的源碼截圖。
getMethodInfo入參member是null,從而導致空指針。需要通過斷點跟蹤運行時的變量值,找到setters數組中的元素是如何生成的。
(4)target是作為對象拷貝的目標對象的類,setters這個數組就是通過反射獲取該目標類的所有具備讀方法的描述對象(PropertyDescriptor對象,可以理解為屬性/方法描述)。這里面方法名有些歧義,不是說只返回getter相關的屬性對象,返回的是該類所有具備讀或寫方法的屬性描述,兩個布爾值的類型分別控制校驗讀或寫。
綜上,由于無法獲取目標類的writeMethod,從而沒有辦法找到這個屬性的寫入方法,就沒有辦法對目標對象繼續賦值。
此時方向就轉到了目標類的實現上,分析到這里就跟Lombok產生了聯系。此處確實被修改過,WaybillProcess類增加了@Accessors這個注解。
@Setter @Getter @Accessors(chain = true) public class WaybillProcess {}
(5)WaybillProcess使用了@Accessors(chain = true)這個注解,這就回到了開頭提到的,在使用了這個注解后該類生成的set方法返回值就不是void而是this,在通過Introspector獲取屬性描述時就不會被認定是寫入方法,在去掉這個注解后,writeMethodName就有值了。
三、解決辦法
解決辦法1:刪除該注解,將工程里鏈式set改成了常規的set賦值方式。
解決辦法2:保留該注解,替換對象拷貝的工具類,建議使用MapStruct配合Lombok,直接在編譯時生成get/set方法,更加安全,功能也更加強大。
四、總結
凡是依賴JDK Introspector獲取類set方法描述的工具類、組件都會受到其寫入方法定義導致的一些列問題,目前在工程實踐中遇到了BeanCopier進行對象拷貝、BeanUtils對屬性進行賦值都會遇到問題。所以大家在日常開發過程中,如果該類已經被大面積的使用,在使用組件特性時需要多留意。
對于對象拷貝已經有很多最佳實踐了,有相關的文章大家可以推薦一下。
感謝閱讀!
審核編輯 黃宇
-
指針
+關注
關注
1文章
481瀏覽量
70611 -
JDK
+關注
關注
0文章
82瀏覽量
16637
發布評論請先 登錄
相關推薦
如何有效的處理空指針異常
函數指針為空的問題
【設計技巧】指針的使用注意事項:空指針、指針賦值、void *指針
Lombok開發插件使用小技巧
重演自己如何掉入Lombok的戲法陷阱
Lombok同時使用@Data和@Builder的一個必須要避開的巨坑
Java注解及其底層原理解析 1
![Java<b class='flag-5'>注解</b>及其底層原理解析 1](https://file.elecfans.com/web2/M00/8F/FF/pYYBAGPkj3eALbGPAAAW-FmQgz8268.jpg)
Java注解及其底層原理解析2
![Java<b class='flag-5'>注解</b>及其底層原理解析2](https://file.elecfans.com/web2/M00/8F/7B/poYBAGPkj3eAJdXcAAFsE5kJ7JY865.jpg)
評論