一、問(wèn)題描述
JaCoCo是一款被廣泛應(yīng)用于公司內(nèi)部的開(kāi)源覆蓋率工具,將其引用至測(cè)試環(huán)境后,機(jī)器啟動(dòng)正常,但在操作下單時(shí)出現(xiàn)異常,阻塞下單流程。
去除JaCoCo配置、重新編譯和部署后下單功能恢復(fù)正常。堆棧信息顯示,問(wèn)題源于系統(tǒng)對(duì)請(qǐng)求字段進(jìn)行加密時(shí)出現(xiàn)異常,因?yàn)闊o(wú)法完成類型轉(zhuǎn)換拋出異常,“[Z cannot be cast to [Ljava.lang.Object”,從而阻塞下單流程。
以下為報(bào)錯(cuò)堆棧信息:
java.lang.ClassCastException: [Z cannot be cast to [Ljava.lang.Object;
at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:93)
at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133)
at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:90)
at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133)
at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:90)
at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133)
at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133)
at com.jd.**.TdeProxy.$$FastClassBySpringCGLIB$$4fa3c52.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769)
..省略
二、問(wèn)題分析
1.報(bào)錯(cuò)代碼
定位報(bào)錯(cuò)信息顯示的代碼位置,確認(rèn)該部分代碼并沒(méi)有被修改過(guò)。報(bào)錯(cuò)提示指出屬性應(yīng)為數(shù)組類型,但在需要加密的類屬性中并沒(méi)有涉及數(shù)組類型的處理。那么“[Z”這個(gè)類型又是從何而來(lái)呢?這種情況下不禁讓人懷疑,在某個(gè)時(shí)刻類可能被修改過(guò)。
報(bào)錯(cuò)信息中的"[Z"代表的是Java中的boolean類型數(shù)組。在Java中,基本數(shù)據(jù)類型的數(shù)組也會(huì)被表示為類似于"[Z"、"[B"、"[L"等形式的字符串,這可能是因?yàn)樵诔绦蜻\(yùn)行過(guò)程中對(duì)類進(jìn)行了動(dòng)態(tài)修改或者反射操作導(dǎo)致的。
以下為報(bào)錯(cuò)處的代碼片段,在將obj轉(zhuǎn)換為Object[]時(shí)出現(xiàn)異常,既然已經(jīng)識(shí)別出是數(shù)組,但是又無(wú)法完成類型轉(zhuǎn)換,具體的原因需要進(jìn)一步分析。
public void encryptObject(Object obj, String type) throws IllegalAccessException {
/***省略***/
if (Map.class.isAssignableFrom(clazz)) {
/***省略***/
} else if(Iterable.class.isAssignableFrom(clazz)) {
/***省略***/
} else if(clazz.isArray()) {
/**********************報(bào)錯(cuò)代碼行****************/
for (Object o : (Object[]) obj) {
/**********************報(bào)錯(cuò)代碼行****************/
this.encryptObject(o, type);
}
} else {
Boolean encryptFlag = null;
Field[] fields = this.getDeclaredFieldsAll(clazz);
for (Field field : fields) {
/***省略***/
}
/***省略***/
for (Field field : fields) {
Class fieldClazz = field.getType();
if (fieldClazz == String.class) {
/***省略***/
} else {
field.setAccessible(true);
Object fieldValue = field.get(obj);
this.encryptObject(fieldValue, type);
}
}
}
}
2.分析路徑
閱讀代碼可以看出encryptObject方法是通過(guò)遞歸實(shí)現(xiàn)的,其主要功能是對(duì)有效集合進(jìn)行遍歷,所以問(wèn)題的重點(diǎn)不是遞歸的過(guò)程,而是推進(jìn)遞歸過(guò)程的元素集合,集合中的元素?zé)o法正常進(jìn)行類型轉(zhuǎn)換導(dǎo)致報(bào)錯(cuò),這就需要檢查getDeclaredFieldsAll方法,該方法在運(yùn)行時(shí)返回的集合中可能包含意料之外的元素,以下為具體實(shí)現(xiàn)代碼:
public Field[] getDeclaredFieldsAll(Class clazz) {
List fieldsList = new ArrayList();
while (clazz != null) {
Field[] declaredFields = clazz.getDeclaredFields();
fieldsList.addAll(Arrays.asList(declaredFields));
clazz = clazz.getSuperclass();
}
return fieldsList.toArray(new Field[fieldsList.size()]);
}
由于已確認(rèn)引入JaCoCo后對(duì)類進(jìn)行了修改,只需觸發(fā)任一流程以獲取類的所有屬性,通過(guò)設(shè)置斷點(diǎn)并觀察集合中的元素,即可查看具體修改情況。

?
?
此時(shí)已經(jīng)可以解釋為什么引入JaCoCo會(huì)導(dǎo)致異常。報(bào)錯(cuò)中的類型“[Z”為合成的屬性,引入JaCoCo會(huì)給類添加一個(gè)名為$jacocoData的bool數(shù)組類型屬性,回到報(bào)錯(cuò)代碼位置,出現(xiàn)報(bào)錯(cuò)是因?yàn)樵谧R(shí)別到一個(gè)數(shù)組類型時(shí)進(jìn)行了類型轉(zhuǎn)換,在這里也找到了問(wèn)題的答案。
涉及到合成屬性/方法和JaCoCo的實(shí)現(xiàn)原理,下面進(jìn)行簡(jiǎn)單的介紹。
3.追本溯源
(1)合成屬性和方法
合成屬性/方法是由Java編譯器在編譯過(guò)程中自動(dòng)生成,并不是研發(fā)顯示編寫的,而是為了支持編譯器內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)而生成的,下面針對(duì)合成方法進(jìn)行一個(gè)舉例說(shuō)明。
public class Pack {
public static void main(String[] args) {
Pack.Goods goods = new Pack.Goods();
System.out.println(goods.name);
}
private static class Goods {
private String name = "手機(jī)";
}
}
將上面的代碼編譯一下,可以看到有三個(gè)文件,Pack$Goods.class、Pack.class、Pack$1.class,前兩個(gè)一個(gè)是內(nèi)部類,一個(gè)是外部類,但是最后一個(gè)類并沒(méi)有被定義過(guò),接下來(lái)分別將內(nèi)部類和外部類進(jìn)行反編譯:
import com.jd.ryan.test.Pack.1;
class Pack$Goods {
private String name;
private Pack$Goods() {
this.name = "手機(jī)";
}
Pack$Goods(1 x0) {
this();
}
static String access$100(Pack$Goods x0) {
return x0.name;
}
}
內(nèi)部類被反編譯后,可以發(fā)現(xiàn)access$100的方法并沒(méi)有被定義,但是分析來(lái)看name是內(nèi)部類Goods的私有屬性,但是外部類可以直接引用這個(gè)屬性,從語(yǔ)法結(jié)構(gòu)上講這是被允許的,這就需要編譯器在編譯過(guò)程處理這種操作,在編譯器看來(lái),外部類和內(nèi)部類是兩個(gè)獨(dú)立的類,如果外部類想要訪問(wèn)內(nèi)部類的私有屬性,其實(shí)是與封裝原則相悖的。那接著看外部類的反編譯結(jié)果:
public class com.jd.ryan.test.Pack {
public com.jd.ryan.test.Pack();
Code:
0: aload_0
1: invokespecial #1 //Method java/lang/Object."":()V
public static void main(java.lang.String[]);
Code:
0: new #2 //class com/jd/ryan/test/Pack$Goods
3: dup
4: aconst_null
5: invokespecial #3 //Method com/jd/ryan/test/Pack$Goods."":(Lcom/jd/ryan/test/Pack$1;V
8: astore_1
9: getstatic #4 //Field java/lang/System.out:Ljava/io/Printstream;
12: aload_1
13: invokestatic #5 //Method com/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods;)Ljava/lang/String;
16: invokevirtual #6//Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: return
}
在代碼實(shí)現(xiàn)中外部類直接打印內(nèi)部類的name屬性值,來(lái)看這行指令:
“Method com/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods;)Ljava/lang/String;”
從字節(jié)碼中表明是通過(guò)調(diào)用了內(nèi)部類的access$100方法,這個(gè)方法是一個(gè)靜態(tài)方法,它可以返回內(nèi)部類的name屬性,是Goods的私有屬性,所以access$100就是編譯器用來(lái)做內(nèi)部訪問(wèn)生成的一個(gè)合成方法。
編譯器可以通過(guò)生成合成屬性和方法來(lái)實(shí)現(xiàn)一些內(nèi)部?jī)?yōu)化或者內(nèi)部實(shí)現(xiàn),所以在使用反射機(jī)制實(shí)現(xiàn)一些工具時(shí),在運(yùn)行時(shí)拿到的類屬性信息還可能會(huì)有一些未知的屬性或者方法,這就需要工具類的代碼具備一定的健壯性,對(duì)獲取到的類屬性進(jìn)行類型轉(zhuǎn)換時(shí)應(yīng)該考慮到非業(yè)務(wù)字段的情況,并且能夠?qū)\(yùn)行時(shí)異常進(jìn)行捕獲,讓工具聚焦在可以處理的范圍,不能影響正常的業(yè)務(wù)流程。
(2)JaCoCo原理簡(jiǎn)述
JaCoCo利用ASM在字節(jié)碼中插入探針指針(Probe指針),每個(gè)探針都是一個(gè)布爾變量(true表示執(zhí)行,false表示未執(zhí)行)。程序運(yùn)行時(shí)通過(guò)修改這些指針來(lái)檢測(cè)代碼的執(zhí)行情況,而不會(huì)改變?cè)即a的行為。提到的$jacocoData數(shù)組用于保存這些執(zhí)行結(jié)果,JaCoCo根據(jù)控制流類型采用不同的探針插入策略,這些探針不會(huì)改變方法的行為,只是記錄它們已經(jīng)執(zhí)行的事實(shí)。
本文不再深入介紹JaCoCo的工作原理,感興趣的同學(xué)可以查閱資料。
三、解決辦法
通過(guò)問(wèn)題分析已經(jīng)確定是$jacocoData導(dǎo)致的,那就需要在獲取屬性集合的的時(shí)對(duì)這類屬性進(jìn)行過(guò)濾,實(shí)現(xiàn)方法通過(guò)isSynthetic()方法區(qū)分field屬性類型,isSynthetic是Java中的一個(gè)修飾符,用于標(biāo)記一個(gè)類、方法或字段是否由編譯器生成。
List fieldsList = Arrays.stream(declaredFields)
.filter(field -> !field.isSynthetic())
.collect(Collectors.toList());
代碼修改后,測(cè)試環(huán)境添加JaCoCo相關(guān)配置,編譯部署發(fā)布后可正常下單,從斷點(diǎn)信息來(lái)看,$jacocoData已經(jīng)被過(guò)濾掉了。

審核編輯 黃宇
-
測(cè)試
+關(guān)注
關(guān)注
8文章
5708瀏覽量
128928 -
開(kāi)源
+關(guān)注
關(guān)注
3文章
3695瀏覽量
43856 -
編譯
+關(guān)注
關(guān)注
0文章
679瀏覽量
34033
發(fā)布評(píng)論請(qǐng)先 登錄
DC/DC轉(zhuǎn)換器的類型與工作原理

【電磁兼容技術(shù)案例分享】因視頻光電轉(zhuǎn)換器導(dǎo)致的BCI問(wèn)題案例

labview數(shù)據(jù)類型與PLC 數(shù)據(jù)類型之間的轉(zhuǎn)換(來(lái)自于寫入浮點(diǎn)數(shù)到匯川 PLC中的數(shù)據(jù)轉(zhuǎn)換關(guān)鍵的修改)
ADC12D500RF是積分型的AD轉(zhuǎn)換器嗎,還是別的什么類型的?
VirtualLab Fusion應(yīng)用:場(chǎng)曲分析儀
【電磁兼容技術(shù)案例分享】LVDS信號(hào)因經(jīng)連接器轉(zhuǎn)接導(dǎo)致RS問(wèn)題整改分析案例

不同類型ACDC轉(zhuǎn)換器優(yōu)缺點(diǎn) ACDC轉(zhuǎn)換器負(fù)載能力分析
不同類型PROM器件的比較分析
不同類型adc的優(yōu)缺點(diǎn)分析
不同類型adc的優(yōu)缺點(diǎn)
同軸轉(zhuǎn)換器為什么容易壞 同軸轉(zhuǎn)換器對(duì)音質(zhì)的影響
不同類型AD轉(zhuǎn)換器的比較
【電磁兼容技術(shù)案例分享】因喚醒線導(dǎo)致的CE電壓法測(cè)試超標(biāo)整改分析案例


評(píng)論