一、問題描述
JaCoCo是一款被廣泛應用于公司內部的開源覆蓋率工具,將其引用至測試環境后,機器啟動正常,但在操作下單時出現異常,阻塞下單流程。
去除JaCoCo配置、重新編譯和部署后下單功能恢復正常。堆棧信息顯示,問題源于系統對請求字段進行加密時出現異常,因為無法完成類型轉換拋出異常,“[Z cannot be cast to [Ljava.lang.Object”,從而阻塞下單流程。
以下為報錯堆棧信息:
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)
..省略
二、問題分析
1.報錯代碼
定位報錯信息顯示的代碼位置,確認該部分代碼并沒有被修改過。報錯提示指出屬性應為數組類型,但在需要加密的類屬性中并沒有涉及數組類型的處理。那么“[Z”這個類型又是從何而來呢?這種情況下不禁讓人懷疑,在某個時刻類可能被修改過。
報錯信息中的"[Z"代表的是Java中的boolean類型數組。在Java中,基本數據類型的數組也會被表示為類似于"[Z"、"[B"、"[L"等形式的字符串,這可能是因為在程序運行過程中對類進行了動態修改或者反射操作導致的。
以下為報錯處的代碼片段,在將obj轉換為Object[]時出現異常,既然已經識別出是數組,但是又無法完成類型轉換,具體的原因需要進一步分析。
public void encryptObject(Object obj, String type) throws IllegalAccessException {
/***省略***/
if (Map.class.isAssignableFrom(clazz)) {
/***省略***/
} else if(Iterable.class.isAssignableFrom(clazz)) {
/***省略***/
} else if(clazz.isArray()) {
/**********************報錯代碼行****************/
for (Object o : (Object[]) obj) {
/**********************報錯代碼行****************/
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方法是通過遞歸實現的,其主要功能是對有效集合進行遍歷,所以問題的重點不是遞歸的過程,而是推進遞歸過程的元素集合,集合中的元素無法正常進行類型轉換導致報錯,這就需要檢查getDeclaredFieldsAll方法,該方法在運行時返回的集合中可能包含意料之外的元素,以下為具體實現代碼:
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()]);
}
由于已確認引入JaCoCo后對類進行了修改,只需觸發任一流程以獲取類的所有屬性,通過設置斷點并觀察集合中的元素,即可查看具體修改情況。
?
?
此時已經可以解釋為什么引入JaCoCo會導致異常。報錯中的類型“[Z”為合成的屬性,引入JaCoCo會給類添加一個名為$jacocoData的bool數組類型屬性,回到報錯代碼位置,出現報錯是因為在識別到一個數組類型時進行了類型轉換,在這里也找到了問題的答案。
涉及到合成屬性/方法和JaCoCo的實現原理,下面進行簡單的介紹。
3.追本溯源
(1)合成屬性和方法
合成屬性/方法是由Java編譯器在編譯過程中自動生成,并不是研發顯示編寫的,而是為了支持編譯器內部的實現細節而生成的,下面針對合成方法進行一個舉例說明。
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 = "手機";
}
}
將上面的代碼編譯一下,可以看到有三個文件,Pack$Goods.class、Pack.class、Pack$1.class,前兩個一個是內部類,一個是外部類,但是最后一個類并沒有被定義過,接下來分別將內部類和外部類進行反編譯:
import com.jd.ryan.test.Pack.1;
class Pack$Goods {
private String name;
private Pack$Goods() {
this.name = "手機";
}
Pack$Goods(1 x0) {
this();
}
static String access$100(Pack$Goods x0) {
return x0.name;
}
}
內部類被反編譯后,可以發現access$100的方法并沒有被定義,但是分析來看name是內部類Goods的私有屬性,但是外部類可以直接引用這個屬性,從語法結構上講這是被允許的,這就需要編譯器在編譯過程處理這種操作,在編譯器看來,外部類和內部類是兩個獨立的類,如果外部類想要訪問內部類的私有屬性,其實是與封裝原則相悖的。那接著看外部類的反編譯結果:
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
}
在代碼實現中外部類直接打印內部類的name屬性值,來看這行指令:
“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;”
從字節碼中表明是通過調用了內部類的access$100方法,這個方法是一個靜態方法,它可以返回內部類的name屬性,是Goods的私有屬性,所以access$100就是編譯器用來做內部訪問生成的一個合成方法。
編譯器可以通過生成合成屬性和方法來實現一些內部優化或者內部實現,所以在使用反射機制實現一些工具時,在運行時拿到的類屬性信息還可能會有一些未知的屬性或者方法,這就需要工具類的代碼具備一定的健壯性,對獲取到的類屬性進行類型轉換時應該考慮到非業務字段的情況,并且能夠對運行時異常進行捕獲,讓工具聚焦在可以處理的范圍,不能影響正常的業務流程。
(2)JaCoCo原理簡述
JaCoCo利用ASM在字節碼中插入探針指針(Probe指針),每個探針都是一個布爾變量(true表示執行,false表示未執行)。程序運行時通過修改這些指針來檢測代碼的執行情況,而不會改變原始代碼的行為。提到的$jacocoData數組用于保存這些執行結果,JaCoCo根據控制流類型采用不同的探針插入策略,這些探針不會改變方法的行為,只是記錄它們已經執行的事實。
本文不再深入介紹JaCoCo的工作原理,感興趣的同學可以查閱資料。
三、解決辦法
通過問題分析已經確定是$jacocoData導致的,那就需要在獲取屬性集合的的時對這類屬性進行過濾,實現方法通過isSynthetic()方法區分field屬性類型,isSynthetic是Java中的一個修飾符,用于標記一個類、方法或字段是否由編譯器生成。
List fieldsList = Arrays.stream(declaredFields)
.filter(field -> !field.isSynthetic())
.collect(Collectors.toList());
代碼修改后,測試環境添加JaCoCo相關配置,編譯部署發布后可正常下單,從斷點信息來看,$jacocoData已經被過濾掉了。
審核編輯 黃宇
-
測試
+關注
關注
8文章
5384瀏覽量
127083 -
開源
+關注
關注
3文章
3408瀏覽量
42719 -
編譯
+關注
關注
0文章
661瀏覽量
33050
發布評論請先 登錄
相關推薦
評論