从ASM看jacoco运行原理

news2024/11/17 14:21:56

前言

我们在开发中如何保证代码质量,我的回答是做充分的代码测试。Jacoco的出发点是为基于JVM运行的代码提供代码覆盖率统计,期望提供轻量级的、可伸缩的、文档较全的库文件来集成各类构建和开发工具。

ASM介绍

ASM 是一个通用的 Java 字节码操作和分析框架。 它可以用于修改现有类或直接以二进制形式动态生成类。 ASM 提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。 ASM 提供与其他 Java 字节码框架类似的功能,但专注于性能。 因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。

ASM增强流程

step1:需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
step2:需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象
step3:然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
step4:当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了

image-20221203211557147

ASM Bytecode Outline 插件

asm是一款偏底层的字节码增强工具,所以在使用的时候需要对字节码指令有一定的了解。我们可以借助asm bytecode outline插件可以帮我们将java文件编译成字节码文件和使用asm指令生成java代码(IDEA2022版本暂不支持)。

我们写一个简单的java实例:

public class Test {
    private int num1 = 1;
    public int add(int a,int b) {
        return a+b;
    }
}

使用asm bytecode outline插件翻译当前文件

image-20221203210938667

我们可以看到当前类对应的字节码文件:

image-20221203211119108

同时也可以看到如何使用ASM工具生成当前类文件,这样我们在用ASM动态生成或者修改类文件的时候就可以以此作为参考。

image-20221203211152139

jacoco源码

jacoco的运行原理也很简单,就是在我们的目标服务代码进行插装,记录代码的执行位置,这样我们能很清晰的看到代码的执行位置,也可以生成覆盖度报告。这样说你可能没有什么概览,我们使用反编译查看被插装的代码你就能大概知道jacoco究竟是如何进行覆盖度收集的了。

在这里插入图片描述

下载jacoco的源码一看,有这么多子moudle,一下子是不是有点懵,不知从何看起呢?其实jacoco实现覆盖度收集的方式有很多种:agent、ant、maven、cli等,再回过头来看代码结构是不是很清晰了很多。其中ant和agent是不会对打包好的jar或war代码产生影响,是在运行时去改变字节码,而maven是在编译的时候就去生成字节码,所以使用maven的方式不适合在生产上使用。

image-20221203211427441

core模块

core作为jacoco的核心模块,完成代码插装、收集、合并等操作。

ExecutionDataStore执行数据集

ExecutionDataStore主要是存储对应的收集数据,由于存储在内存中,所以在停机之前如果不进行收集,则测试数据会被丢失。entries是一个Map,id是对应class的唯一标识,ExecutionData是对应类的覆盖数据。

image-20221204141644244

ExecutionData数据结构如下:

public final class ExecutionData {

	private final long id;
	private final String name;
	private final boolean[] probes;

	public ExecutionData(final long id, final String name,
			final boolean[] probes) {
		this.id = id;
		this.name = name;
		this.probes = probes;
	}

	public void reset() {
		Arrays.fill(probes, false);
	}

	public boolean hasHits() {
		for (final boolean p : probes) {
			if (p) {
				return true;
			}
		}
		return false;
	}
	
	public void merge(final ExecutionData other, final boolean flag) {
		assertCompatibility(other.getId(), other.getName(),
				other.getProbes().length);
		final boolean[] otherData = other.getProbes();
		for (int i = 0; i < probes.length; i++) {
			if (otherData[i]) {
				probes[i] = flag;
			}
		}
	}

}
  • id:对应class唯一标识。

  • name:对应class的name。

  • probes:打桩数据,是一个boolean的数组,初始化时为全为false,如果有相应位置的代码被执行,则对应位置的数据变为true。

  • reset():清空测试覆盖数据。

  • hasHits():检查是否有任何探针被击中。

  • merge():将同一个类的两次收集数据合并,合并的逻辑就是如果有对应下标为true,则合并后的数据就为flag,flag可以为true也可以为false。

Instrumenter插装

Instrumenter是负责处理插装核心逻辑,它接受的是源class字节码数据,返回含插装数据的字节数组。

private byte[] instrument(final byte[] source) {
		final long classId = CRC64.classId(source);
		final ClassReader reader = InstrSupport.classReaderFor(source);
		final ClassWriter writer = new ClassWriter(reader, 0) {
			@Override
			protected String getCommonSuperClass(final String type1,
					final String type2) {
				throw new IllegalStateException();
			}
		};
		final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
				.createFor(classId, reader, accessorGenerator);
		final int version = InstrSupport.getMajorVersion(reader);
		final ClassVisitor visitor = new ClassProbesAdapter(
				new ClassInstrumenter(strategy, writer),
				InstrSupport.needsFrames(version));
		reader.accept(visitor, ClassReader.EXPAND_FRAMES);
		return writer.toByteArray();
	}

我们再回想一下前面使用反编译的类信息,多了哪些信息呢?分别是成员变量jacocoData, jacocoInit方法,以及方法中为jacocoData数组赋值,那我们就看下是在什么时机去完成上面三个操作的。

jacocoData数组赋值

org.jacoco.core.internal.instr.ProbeInserter#insertProbe这个方法就是给数组赋值的,并且值为true。java中方法被调用,对应一次入栈与出栈操作,那么对应的指令也需要进行入栈,所以通过相应位置加入额外的入栈指令,达到字节码增强的目的。

public void insertProbe(final int id) {
		mv.visitVarInsn(Opcodes.ALOAD, variable);
		// Stack[0]: [Z
		InstrSupport.push(mv, id);
		// Stack[1]: I
		// Stack[0]: [Z
		mv.visitInsn(Opcodes.ICONST_1);
		// Stack[2]: I
		// Stack[1]: I
		// Stack[0]: [Z
		mv.visitInsn(Opcodes.BASTORE);
	}

接下来我们看下insertProbe方法在哪些地方被调用:MethodInstrumenter。

image-20221204150950244

这里还需要提一下ASM 的Label,是实现条件语句跳转的。换句话说,我们需要知道在方法的哪些位置插装。

public void test(){
	// ①
	int a = 1;
	// ②
	int b = 2;
	// ③
	if(a > 2){
		// ④
	}else{
		// ⑤
	}
}

我们看下上面的代码,我们并不是需要在每一个位置都进行插装,比如在①插装②、③就没必须进行插装了,因为②、③是一定会执行到的;所以我们需要插装的位置就是①、④、⑤。

jacocoData和jacocoInit

org.jacoco.core.internal.flow.ClassProbesAdapter#visitEnd,在类被加载完毕的时候执行visitEnd,最后调用org.jacoco.core.internal.instr.InterfaceFieldProbeArrayStrategy#addMembers,这里我们已经清楚jacocoData和jacocoInit的生成时机了,生成的逻辑也是使用ASM增强。

image-20221204152042226

agent插装调度

agent的执行入口是premain方法,如何开发和调试agent这里就不多说了,可以自行百度。我们看看jacoco agent方式是如何进行插装的。

org.jacoco.agent.rt.internal.PreMain#premain

image-20221203220900734

由此可以看出,jacoco是在org.jacoco.agent.rt.internal.CoverageTransformer中完成插装任务的。

image-20221204152431598

这里先调用了dump方法是为了将源字节码信息存储下来,方便后面清除插装数据;然后我们在transform方法中看到了调用Instrumenter,这里是不是又很熟悉了,开始调用core模块的插装接口。

maven插装调度

maven的插装入口是InstrumentMojo,这里需要有maven插件开发基础,不然的话可能不太理解运行原理。

@Mojo(name = "instrument", defaultPhase = LifecyclePhase.PROCESS_CLASSES, threadSafe = true)
public class InstrumentMojo extends AbstractJacocoMojo {

	@Parameter
	private List<String> includes;

	@Parameter
	private List<String> excludes;

	@Override
	public void executeMojo()
			throws MojoExecutionException, MojoFailureException {
		final File originalClassesDir = new File(
				getProject().getBuild().getDirectory(),
				"generated-classes/jacoco");
		originalClassesDir.mkdirs();
		final File classesDir = new File(
				getProject().getBuild().getOutputDirectory());
		if (!classesDir.exists()) {
			getLog().info(
					"Skipping JaCoCo execution due to missing classes directory:"
							+ classesDir);
			return;
		}
		final List<String> fileNames;
		try {
			fileNames = new FileFilter(includes, excludes)
					.getFileNames(classesDir);
		} catch (final IOException e1) {
			throw new MojoExecutionException(
					"Unable to get list of files to instrument.", e1);
		}

		final Instrumenter instrumenter = new Instrumenter(
				new OfflineInstrumentationAccessGenerator());
		for (final String fileName : fileNames) {
			if (fileName.endsWith(".class")) {
				final File source = new File(classesDir, fileName);
				final File backup = new File(originalClassesDir, fileName);
				InputStream input = null;
				OutputStream output = null;
				try {
					FileUtils.copyFile(source, backup);
					input = new FileInputStream(backup);
					output = new FileOutputStream(source);
					instrumenter.instrument(input, output, source.getPath());
				} catch (final IOException e2) {
					throw new MojoExecutionException(
							"Unable to instrument file.", e2);
				} finally {
					IOUtil.close(input);
					IOUtil.close(output);
				}
			}
		}
	}
}

从上面代码可以看到调用Instrumenter的地方,与探针插装不同的是maven方式拿到插装的字节数据是写入到文件中,其他也就是编译后的产物。

其他指令

除了插状之外,jacoco还提供了dump、merge、restore、report等指令,基于上面的基础,不难分析出它的工作原理。

image-20221204154210026

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/60808.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

架构设计(消息队列)

架构设计&#xff08;消息队列&#xff09; 消息队列 发送者将消息发送到topic&#xff0c;消费者从topic中拉取消息进行消费 发送端消息发送方式 同步发送&#xff1a;消息发送后&#xff0c;需要等待消息发送响应结果&#xff0c;发送失败可重试 异步发送&#xff1a;消息发…

Numpy入门[17]——数组广播机制

Numpy入门[17]——数组广播机制 参考&#xff1a; https://ailearning.apachecn.org/NumPy广播机制 使用Jupyter进行练习 NumPy 中的广播机制&#xff08;Broadcast&#xff09;旨在解决不同形状数组之间的算术运算问题。我们知道&#xff0c;如果进行运算的两个数组形状完全相…

linux网络编程epoll详解

目录epoll原理解析epoll提供的接口epoll的触发模式epoll原理解析 从socket接收网络数据说起&#xff1a; 1、网络传输中&#xff0c;网卡会把接收到的数据写入内存&#xff0c;网卡向 CPU 发出一个中断信号&#xff0c;操作系统便能得知有新数据到来&#xff0c;再通过网卡中断…

第二证券|行业重磅白皮书发布,超高清视频产业规模剑指3万亿

在5G和超高清交融开展的布景下&#xff0c;下流使用需求有望迸发&#xff0c;超高清视频工业前景可观。 超高清工业规模有望突破3万亿 据报道&#xff0c;12月1日&#xff0c;2022国际显现工业大会分论坛——新式显现超高清主题论坛在成都举行。论坛上&#xff0c;中国电子信息…

文本编辑器vi--常用命令查阅版(记得收藏)

一.为何要学习vi   # 所有的UNIX-like系统都会内置vi文本编辑器&#xff0c;其他的文本编辑器则不一定会存在&#xff1b;   # 很多软件的编辑接口都会主动调用vi&#xff1b;   # vim具有程序编辑的能力&#xff0c;可以主动地以字体颜色辨别语法的正确性&#xff0c;方…

双元科技过会:计划募资6.5亿元,比亚迪和蜂巢能源为主要客户

近日&#xff0c;上海证券交易所披露的信息显示&#xff0c;浙江双元科技股份有限公司&#xff08;下称“双元科技”&#xff09;获得科创板上市委会议通过&#xff08;即IPO过会&#xff09;。接下来&#xff0c;双元科技将提交注册。 据贝多财经了解&#xff0c;双元科技于20…

关于天干地支及其计算

以天干地支计算日期是我国悠良的传统文化&#xff0c;最近在看如何计算人的生辰八字&#xff0c;写了个程序&#xff0c;但是只能算年的干支&#xff0c;月、日的干支计算方法太复杂了&#xff0c;望之只能却步&#xff0c;还是乖乖去查万年历比较好。这里记下关于干支的一些东…

[附源码]Python计算机毕业设计Django框架的资产管理系统设计与实现

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

[附源码]Python计算机毕业设计SSM京津冀区域产学研项目管理信息系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

50、IO流

*学习的难点&#xff1a;要知道在什么情况&#xff0c;该用什么流 补&#xff1a;ANSI码就是gbk码 一、基本概念&#xff1a; 1、什么是文件&#xff1a; 文件是保存数据的地方 2、文件流&#xff1a; 文件在程序中是以流的形式来操作的 &#xff08;1&#xff09;流&am…

XXL-Job海量数据处理-分片任务实战

文章目录一、需求1. 场景2. 分析3. 案例二、什么是分⽚任务2.1. 分⽚路由策略2.2. 海量数据处理2.3. 分片数量2.4. 分片值颁发2.5. 案例三、解决思路3.1. 数据拆分3.2. 分片数量3.3. 分⽚⽅式3.4. 路由策略3.5. 程序实战一、需求 1. 场景 有⼀个任务需要处理100W条数据&#…

【JavaScript 逆向】极验四代无感验证码逆向分析

前言 四代无感验证码相较于滑块验证码区别就是没有底图&#xff0c;一键通过模式&#xff0c;所以不需要轨迹以及计算缺口距离&#xff0c;步骤更少&#xff0c;四代滑块可以阅读&#xff1a;【JavaScript 逆向】极验四代滑块验证码逆向分析 声明 本文章中所有内容仅供学习交…

C++最后一次实验及实验总结

忙活了大半个学期&#xff0c;终于学完了C&#xff0c;虽然很基础&#xff0c;但是至少算是写完了实验&#xff0c;开心~~ 实验一 实验二 实验三 实验四 实验五 实验六 题目一 一、分析下面的程序&#xff0c;写出其运行时的输出结果。上机运行该程序&#xff0c;观察运行…

[附源码]计算机毕业设计ssm新能源电动汽车充电桩服务APP

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

ROS action客户端和服务端通信(Ubuntu )

ROS action客户端和服务端通信 gcusms ROS 一般都是用 service 和 topic 进行数据之间的交互传输&#xff0c;因为这种通信方式无法满数据实时反馈的要求&#xff0c;所以采用 action 动作消息反馈通信机制&#xff08;实时反馈的任务进度&#xff0c;并且可以随时终止运行&am…

用 AWTK 和 AWPLC 快速开发嵌入式应用程序 (6)-在线调试

AWPLC 目前还处于开发阶段的早期&#xff0c;写这个系列文章的目的&#xff0c;除了用来验证目前所做的工作外&#xff0c;还希望得到大家的指点和反馈。如果您有任何疑问和建议&#xff0c;请在评论区留言。 1. 背景 AWTK 全称 Toolkit AnyWhere&#xff0c;是 ZLG 开发的开源…

vuex学习记录

为什么要用vuex 由于vue本身的特点。及页面是由多个组件构成。而组件又呈现一个二叉树状态。然后父向子需要进行通信。那如果是非父子关系&#xff0c;应该如何传值呢&#xff1f; 什么是vuex 专门为vue.js应用程序开发的状态管理模式。它采用集中式存储管理数据&#xff0c…

详解 Go 语言中的 init () 函数

阅读目录Go init 函数的详细说明包初始化Go init 函数的详细说明 初始化每个包后&#xff0c;会自动执行 init&#xff08;&#xff09;函数&#xff0c;并且执行优先级高于主函数的执行优先级。 init 函数通常用于&#xff1a; 变量初始化检查 / 修复状态注册器运行计算 包…

c语言零基础入门(完整版)

1软件下载 官网下载: https://sourceforge.net/projects/orwelldevcpp/ 百度网盘&#xff1a;https://pan.baidu.com/s/1mhHDjO8 提取密码&#xff1a;mken 推荐用百度网盘&#xff0c;官方下载太慢了 开始安装 首先双击打开刚刚下载的软件 点击0k 因为在安装过程中不能使用…

【计算机视觉】图像形成与颜色

图像形成与颜色 光照及阴影 辐射度学 颜色 颜色信息反映了入射光的能量分布与波长&#xff0c;可见光的波长在400nm到760nm之间。 RGB RGB分别代表三个基色&#xff08;R-红色、G-绿色、B-蓝色&#xff09;&#xff0c;如(0,0,0)表示黑色、(255, 255, 255)表示白色。其中2…