服务增加 jacoco 后出现数组越界异常

一 问题现象

测试环境访问 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,从而造成了数组越界。
得到的结论是必须修改对应代码,没有其他解决办法。

三 总结与收获

后续再引入新的第三方开源软件时必须透彻了解其原理后方能推出使用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值