一 问题现象
测试环境访问 wb-liveroom 接口出现 HTTP CODE 500 错误。错误日志中显示 java.lang.ArrayIndexOutOfBoundsException 异常类型。
二 排查过程
测试环境对于 wb-liveroom 新增了 jacoco(on-the-fly) 客户端进行代码覆盖率检测。去掉 jacoco 后接口可正常返回。判断与 jacoco 有关。
因问题接口代码中存在反射,jacoco FAQ 中提到过关于反射的解决方案。
在反射代码中增加对于合成变量的判断即可解决该问题。但是需要修改源码,因为推进代码覆盖率的初期目标是"无侵入"的,所以需要排查出 ArrayIndexOutOfBoundsException 的根本原因,从而决定是否真的需要修改源码。
通过对于问题接口的分析,提取出了必现问题的 demo。
// TestClass1.java
import lombok.Data;
@Data
public class TestClass1 {
private String s1;
}
// TestClass2.java
public class TestClass2 {
}
// test.java
import java.lang.reflect.Field;
public class test {
public static void main(String[] args) {
TestClass2 t2 = new TestClass2();
TestClass1 t1 = null;
t1 = copyFields(t2, new TestClass1());
System.out.println(t1);
}
public static <T> T copyFields(Object sourceObject, T targetObject) {
if (targetObject == null) {
return targetObject;
}
if (sourceObject == null) {
return targetObject;
}
try {
Field[] targetFields = targetObject.getClass().getDeclaredFields();
if (targetFields == null || targetFields.length <= 0) {
return targetObject;
}
Field[] sourceFields = sourceObject.getClass().getDeclaredFields();
for (Field field : targetFields) {
field.setAccessible(true);
for (Field sourceField : sourceFields) {
if (sourceField.getName().equals(field.getName())) {
sourceField.setAccessible(true);
Object fieldNewValue = sourceField.get(sourceObject);
field.set(targetObject, fieldNewValue);
break;
}
}
}
} catch (Exception e) {
System.out.println(e);
}
return targetObject;
}
}
其中 copyFields 方法是业务代码公共库中拷贝所得。
通过 jad 提取了对应代码被 jacoco 插桩后的代码。
// TestClass1.java
/*
* Decompiled with CFR.
*/
package com.rdpaas.debugger.test.bean;
import lombok.Generated;
public class TestClass1 {
private String s1;
private static transient /* synthetic */ boolean[] $jacocoData;
@Generated
public TestClass1() {
boolean[] blArray = TestClass1.$jacocoInit();
blArray[0] = true;
}
@Generated
public String getS1() {
boolean[] blArray = TestClass1.$jacocoInit();
blArray[1] = true;
return this.s1;
}
/*
* WARNING - void declaration
*/
@Generated
public void setS1(String string) {
void s1;
boolean[] blArray = TestClass1.$jacocoInit();
this.s1 = s1;
blArray[2] = true;
}
/*
* Unable to fully structure code
*/
@Generated
public boolean equals(Object var1_1) {
block7: {
block5: {
block6: {
var2_2 = TestClass1.$jacocoInit();
if (o == this) {
var2_2[3] = true;
return true;
}
if (!(o instanceof TestClass1)) {
var2_2[4] = true;
return false;
}
other = (TestClass1)o;
if (!other.canEqual(this)) {
var2_2[5] = true;
return false;
}
this$s1 = this.getS1();
other$s1 = other.getS1();
if (this$s1 != null) break block5;
if (other$s1 != null) break block6;
var2_2[6] = true;
break block7;
}
var2_2[7] = true;
** GOTO lbl26
}
if (this$s1.equals(other$s1)) {
var2_2[8] = true;
} else {
var2_2[9] = true;
lbl26:
// 2 sources
var2_2[10] = true;
return false;
}
}
var2_2[11] = true;
return true;
}
/*
* WARNING - void declaration
*/
@Generated
protected boolean canEqual(Object object) {
void other;
boolean[] blArray = TestClass1.$jacocoInit();
blArray[12] = true;
return other instanceof TestClass1;
}
@Generated
public int hashCode() {
int n;
boolean[] blArray = TestClass1.$jacocoInit();
/* 5*/ int PRIME = 59;
int result = 1;
String $s1 = this.getS1();
if ($s1 == null) {
n = 43;
blArray[13] = true;
} else {
n = $s1.hashCode();
blArray[14] = true;
}
result = result * 59 + n;
blArray[15] = true;
return result;
}
@Generated
public String toString() {
boolean[] blArray = TestClass1.$jacocoInit();
blArray[16] = true;
return "TestClass1(s1=" + this.getS1() + ")";
}
private static /* synthetic */ boolean[] $jacocoInit() {
boolean[] blArray = $jacocoData;
if ($jacocoData == null) {
Object[] objectArray = new Object[]{-783289383302732071L, "com/rdpaas/debugger/test/bean/TestClass1", 17};
UnknownError.$jacocoAccess.equals(objectArray);
blArray = $jacocoData = (boolean[])objectArray[0];
}
return blArray;
}
}
// TestClass2.java
/*
* Decompiled with CFR.
*/
package com.rdpaas.debugger.test.bean;
public class TestClass2 {
private static transient /* synthetic */ boolean[] $jacocoData;
public TestClass2() {
boolean[] blArray = TestClass2.$jacocoInit();
blArray[0] = true;
}
private static /* synthetic */ boolean[] $jacocoInit() {
boolean[] blArray = $jacocoData;
if ($jacocoData == null) {
Object[] objectArray = new Object[]{-7902152578753779585L, "com/rdpaas/debugger/test/bean/TestClass2", 1};
UnknownError.$jacocoAccess.equals(objectArray);
blArray = $jacocoData = (boolean[])objectArray[0];
}
return blArray;
}
}
代码可见每一个 class 中分别被 jacoco 增加了 $jacocoData 属性用于存储被执行代码的标识。新增了 $jacocoInit 方法实现了单例模式,每次会修改 $jacocoData 数组中的值。
在 test.java 中分别实例化了 TestClass1、TestClass2,TestClass1.java 中的 $jacocoData 数组长度为 17,TestClass2.java 中的 $jacocoData 数组长度为 1。
getDeclaredFields() ( $jacocoData)
对象 t2 copy 到 t1 时,因为都存在合成变量 $jacocoData,触发了 set 操作。set 操作中将 t2 数组长度为 1 的 $jacocoData 覆盖了 t1 数组长度为 17的 $jacocoData。
set 会隐式调用 t1 的 toString() 方法,此时 t1 的 $jacocoData 数组长度为 1,而 toString() 的插桩是 [16] = True,从而造成了数组越界。
得到的结论是必须修改对应代码,没有其他解决办法。
三 总结与收获
后续再引入新的第三方开源软件时必须透彻了解其原理后方能推出使用。