最近业务部门开始推行,在全部后台应用中自动开启覆盖率测试。然而,不久后就有业务测试的同学反馈出现问题。
问题的现象如下:
我们的业务通过 HTTP 调用腾讯OSS的服务,结果得到了以上的报错信息。测试同学验证后发现,关闭覆盖率测试后,问题就消失了。因此,我们可以大致确定是覆盖率引起的问题。
按照经验,覆盖率引起的问题基本上只有一种情况。这里我们可以看看官方文档中的FAQ。
我的代码使用反射。为什么用JaCoCo执行会失败?
为了收集执行数据,JaCoCo 检测被测类,它向类添加两个成员:一个私有静态字段
$jacocoData
和一个私有静态方法$jacocoInit()
。两个成员都被标记为合成的。请更改您的代码以忽略合成成员。无论如何,这是一个很好的做法,因为 Java 编译器在某些情况下也会创建合成成员。
但这些仅是我猜测的原因。我们需要具体问题,具体分析才行。这个问题比较容易分析,因为我们可以发现报错的原因是传递的参数不合法,所以才会有这样的错误提示。因此,只需对比开启覆盖率和未开启覆盖率两次请求的数据,就可以查看两次请求的差异,进而分析影响请求内容的代码逻辑。
具体分析
尝试1: arthas
private HttpResponse executeOneRequest(HttpContext context, HttpRequestBase httpRequest) {
HttpResponse httpResponse = null;
try {
httpResponse = httpClient.execute(httpRequest, context);
} catch (IOException e) {
httpRequest.abort();
throw ExceptionUtils.createClientException(e);
}
return httpResponse;
}
跟踪代码我们发现,最后qcloud
的http接口执行会到这里来,而刚好,它这里有一个入参数 httpRequest
, 那就可以通过arthas
去watch 一下看看对应的入参的差异内容啦。
watch com.qcloud.cos.http.DefaultCosHttpClient executeOneRequest '{params,returnObj,throwExp}' -n 5 -x 3
我们看下打印的结果数据
我们发现请求的内容是一个流数据,所以想要通过arthas打印出内容,感觉不太可行了。只能换一种方式
尝试2: 抓包
在抓包上呢 又有一个问题,因为我们请求的域名是一个https的,所以想要抓包具体的包的内容,就有点困难了,所以只能尝试去修改下请求的地址跟协议,因为我们的目的也只是为了能够看到请求的内容而已。
最后我们发现两个请求的差异的内容
插桩后的请求xml格式内容
<Request>
<Tag>Transcode</Tag>
<BucketName>cos-public-1304449511</BucketName>
<QueueId>p870dd99714054da5b311bc70d3110bf9</QueueId>
<CallBack>http://xxx/cstore/api/v3/callback/async/task/tencent</CallBack>
<CallBackFormat>json</CallBackFormat>
<Input>
<Object>dev-cos-public/90b73c9786584f5e9e7748a73b7bf984.mp3</Object>
</Input>
<Operation>
<Watermark></Watermark>
<RemoveWatermark></RemoveWatermark>
<ConcatTemplate>
<Video></Video>
<Audio></Audio>
</ConcatTemplate>
<Transcode>
<Container>
<Format>mp4</Format>
</Container>
<TimeInterval></TimeInterval>
<Video></Video>
<Audio>
<Codec>aac</Codec>
</Audio>
<TransConfig></TransConfig>
</Transcode>
<DigitalWatermark></DigitalWatermark>
<Output>
<Region>ap-shanghai</Region>
<Object>dev-cos-public/b8a483f8b61e4d27baec298752416c1c.mp4</Object>
<Bucket>cos-public-1304449511</Bucket>
</Output>
<PicProcess></PicProcess>
<Snapshot>
<SpriteSnapshotConfig></SpriteSnapshotConfig>
</Snapshot>
<Segment>
<HlsEncrypt></HlsEncrypt>
</Segment>
<SmartCover></SmartCover>
<VideoMontage>
<Video></Video>
<Audio></Audio>
<AudioMix></AudioMix>
</VideoMontage>
</Operation>
</Request>
未插桩的请求数据
<Request>
<Tag>Transcode</Tag>
<BucketName>cos-public-1304449511</BucketName>
<QueueId>p870dd99714054da5b311bc70d3110bf9</QueueId>
<CallBack>http://cstore-dev.test.seewo.com/cstore/api/v3/callback/async/task/tencent</CallBack>
<CallBackFormat>json</CallBackFormat>
<Input>
<Object>dev-cos-public/90b73c9786584f5e9e7748a73b7bf984.mp3</Object>
</Input>
<Operation>
<Transcode>
<Container>
<Format>mp4</Format>
</Container>
<Audio>
<Codec>aac</Codec>
</Audio>
</Transcode>
<Output>
<Region>ap-shanghai</Region>
<Object>dev-cos-public/e5ef796313e0490f9571ede6758253a2.mp4</Object>
<Bucket>cos-public-1304449511</Bucket>
</Output>
</Operation>
</Request>
通过上述的对比,我们就会发现,插桩后的xml请求多出来了很多多余的标签,那我们就要回到代码里面去查看,这个标签是在什么时候被添加的了。
认真查看代码后,我们发现增加具体的标签逻辑是在以下的代码中进行的
private static void addOperation(XmlWriter xml, MediaJobsRequest request) {
MediaJobOperation operation = request.getOperation();
xml.start("Operation");
addIfNotNull(xml, "TemplateId", operation.getTemplateId());
addWatermarkTemplateId(xml, operation.getWatermarkTemplateId());
addWatermar(xml, operation.getWatermark());
addWatermarList(xml, operation.getWatermarkList());
addRemoveWatermark(xml, operation.getRemoveWatermark());
addConcat(xml, operation.getMediaConcatTemplate());
addTranscode(xml, operation.getTranscode());
addExtractDigitalWatermark(xml, operation.getExtractDigitalWatermark());
addMediaDigitalWatermark(xml, operation.getDigitalWatermark());
addOutput(xml, operation.getOutput());
addPicProcess(xml, operation.getPicProcess());
addSnapshot(xml, operation.getSnapshot());
addSegment(xml, operation.getSegment());
addSmartCover(xml, operation.getSmartCover());
addVideoMontage(xml, operation.getVideoMontage());
xml.end();
}
我们看下 addWatermar
的逻辑看看
private static void addWatermar(XmlWriter xml, MediaWatermark watermark) {
if (objIsNotValid(watermark)) {
xml.start("Watermark");
addIfNotNull(xml, "Type", watermark.getType());
addIfNotNull(xml, "Dx", watermark.getDx());
addIfNotNull(xml, "Dy", watermark.getDy());
addIfNotNull(xml, "EndTime", watermark.getEndTime());
addIfNotNull(xml, "LocMode", watermark.getLocMode());
addIfNotNull(xml, "Pos", watermark.getPos());
addIfNotNull(xml, "StartTime", watermark.getStartTime());
if ("Text".equalsIgnoreCase(watermark.getType())) {
MediaWaterMarkText text = watermark.getText();
xml.start("Text");
addIfNotNull(xml, "FontColor", text.getFontColor());
addIfNotNull(xml, "FontSize", text.getFontSize());
addIfNotNull(xml, "FontType", text.getFontType());
addIfNotNull(xml, "Text", text.getText());
addIfNotNull(xml, "Transparency", text.getTransparency());
xml.end();
} else if ("Image".equalsIgnoreCase(watermark.getType())) {
MediaWaterMarkImage image = watermark.getImage();
xml.start("Image");
addIfNotNull(xml, "Height", image.getHeight());
addIfNotNull(xml, "Mode", image.getMode());
addIfNotNull(xml, "Transparency", image.getTransparency());
addIfNotNull(xml, "Url", image.getUrl());
addIfNotNull(xml, "Width", image.getWidth());
xml.end();
}
xml.end();
}
}
我们会发现,这里的重点是在于 objIsNotValid
只有这个为true
才会去添加 Watermark 的标签的。
public static Boolean objIsNotValid(Object obj) {
//查询出对象所有的属性
Field[] fields = obj.getClass().getDeclaredFields();
//用于判断所有属性是否为空,如果参数为空则不查询
for (Field field : fields) {
//不检查 直接取值
field.setAccessible(true);
try {
Object o = field.get(obj);
if (!isEmpty(o)) {
//不为空
return true;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return false;
}
代码的解释也很清楚了,就是通过反射的方式判断所传递的对象中的所有成员变量的属性值是否为空,如果不为空,就会进行添加水印等操作。
我们也断点去看下。
结合上图,很容易就能看出,MediaWaterMark
类在插桩后,将通过反射获取到多一个 $jacococData
成员变量。由于它不为空,前面的判断逻辑就会出现问题,导致水印标签被添加上去。
解决这个问题非常简单,因为网上已经有很多相应的解决措施了。
public static Boolean objIsNotValid(Object obj) {
//查询出对象所有的属性
Field[] fields = obj.getClass().getDeclaredFields();
//用于判断所有属性是否为空,如果参数为空则不查询
for (Field field : fields) {
//不检查 直接取值
field.setAccessible(true);
try {
Object o = field.get(obj);
// 如果是一个合成变量就跳过
if (field.isSynthetic()) {
continue;
}
if (!isEmpty(o)) {
//不为空
return true;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return false;
}
总结
本文讨论了在使用jacoco覆盖率工具时,由于其插桩导致的反射问题。通过分析传递参数不合法的错误提示,比较开启和未开启覆盖率两次请求的数据,发现插桩后的请求多出了很多多余的标签,最终发现是由于插桩后的类中多了一个 $jacococData
成员变量导致的。解决方法是在判断对象属性是否为空时,跳过合成变量。