jacoco agent 另类2次开发
执念
jacoco,一个至今还在不断更新的开源覆盖率工具,第一听到它是因为有一次公司需要招一个业务测试leader,然后公司安排我进行一面。在面试过程中,我像一个学生虚心的聆听着老师的介绍(然而我并没有给他pass)。随后我各个方式都体验了下jacoco,也基于它在公司从0到1落地了个覆盖率平台。但是我一直有一个执念,虽然平台出来了也能用,但是还是感觉羁绊太多,想要更(hua)加(li)丝(hu)滑(shao)。
老(传统)流程
老流程非常的原生态,完全按jacoco的预设套路来。

它从接入到获取覆盖率的数据大致步骤如下:
-
在构建应用发布镜像的时候放入jacocoagent.jar
-
在发布应用动作发生的开始阶段,通过接口方式(向覆盖率管理端)获取当前发布应用的jacoco配置参数
-
在发布应用的同时,给覆盖率管理端的服务器上下载 class文件
-
先拉取RuntimeData
-
由覆盖率服务端结合下载的class文件解析RuntimeData
这里其实存在几个问题,应用发布操作已经受到了影响。操作链路过长,任何一个小步骤出现问题,都会导致不稳定(甚至不可用),例如class文件没同步,class版本拿错等等。虽然之前玩的好好的,但是经历了几次小插曲后(新的部署方式加入,老发布系统改造等等),我就萌生出了点其他的想法。有没有可能让协作方都不再这么累,用更加少的步骤,更快捷,更精准,更稳定?
新(另类)流程
肯定是可能的,不然我干嘛说呢?新的流程会把之前的流程优化的更简单:

-
在构建应用发布镜像的时候放入jacocoagent.jar
-
有jacocoagent插件自己,通过接口方式(向覆盖率管理端)获取当前发布应用的jacoco配置参数
-
由jacocoagent直接结合服务器上的class文件解析本地的RuntimeData内存对象,并上报给服务端。
二者的区别
| 操作 | 老流程 | 新流程 |
| 准备字节码文件 | 管理端需要准备一份class文件 | 不需要 |
| 拉取RuntimeData | 从本地dump并发向管理端 | 不需要 |
| 把RuntimeData进行解析 | 管理端根据class文件和RunTimeData进行解析 | 直接在服务器上使用RuntimeData和服务器的class文件进行解析 |
| 把解析数据发送给管理端 | 不需要 | 通过上报解析后的数据给管理端 |
有没有发现,其实最终的结果是一样的,过程也都是有的,都是最后获取到解析完的数据而已。所有的流程都还是走了的,就是顺序上看似有了些变化。Talk is cheap. 那我们来看下大致实现的切入点有哪些,一共有4处,最后一处改动尤其重要。
第一,我们需要一个基础能力发送数据给管理端。
private static void postObject(ExecutionDataStore store) {CoverageBuilder builder = new CoverageBuilder();//这里做了一个小小的改动,为了解决因为包类冲突导致覆盖率数据解析失败的问题。本文不做过多介绍Analyzer analyzer = new Analyzer(store, builder, excludes, includes);try {// jacoco 使用 class 文件和本地rumtimeData对象进行覆盖率解析的方法// JARPATH 是改变AgentOptions 增加的外部传入参数,特指应用服务的jar包位置analyzer.analyzeAll(new File(JARPATH));} catch (IOException ignored) {ignored.printStackTrace();}DEFAULTHEADERS.put("Content-type", "application/octet-stream");IBundleCoverage iBundleCoverage = builder.getBundle(JARPATH);try {// 通过http的方式把覆盖率详细信息,通过 byte[] 的方式压缩并发回给管理端。(压缩后10M -> 500K)post(URL + PATHPOSTDATA + APPNAME + "/" + IP, compress(iBundleCoverage.toJSONString().getBytes(StandardCharsets.UTF_8)), DEFAULTHEADERS);} catch (IOException e) {e.printStackTrace();}public static void post(ExecutionDataStore dataStore) {// URL 为管理端的接口url,Ip 为当前应用的ip地址if ( URL == null || IP == null) {return;}try {Thread thread = new Thread(() -> {try {postObject(dataStore);} catch (Exception ignored) {ignored.printStackTrace();}});thread.start();} catch (Exception ignored) {ignored.printStackTrace();}}public static byte[] compress(byte[] input) {// 使用jdk的方式进行压缩Deflater compressor = new Deflater(Deflater.BEST_COMPRESSION, false);compressor.setInput(input);compressor.finish();try (ByteArrayOutputStream bao = new ByteArrayOutputStream()) {byte[] readBuffer = new byte[1024];int readCount = 0;while (!compressor.finished()) {readCount = compressor.deflate(readBuffer);if (readCount > 0) {bao.write(readBuffer, 0, readCount);}}compressor.end();return bao.toByteArray();} catch (Exception ignored) {}return new byte[0];}}
第二,改造AgentOptions类,添加新字段方便控制,比如去哪里找class文件,怎么获得应用的配置等
public final class AgentOptions {...public static final String APPNAME = "appname";public static final String CLOUD = "cloud";public static final String JARPATH = "jar";...public static final Collection<String> VALID_OPTIONS = Arrays.asList(DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER,INCLBOOTSTRAPCLASSES, INCLNOLOCATIONCLASSES, SESSIONID, DUMPONEXIT,OUTPUT, ADDRESS, PORT, CLASSDUMPDIR, JMX, APPNAME, CLOUD, JARPATH);...// 因为 jacoco 方法封装的很方便扩展,这边就不需要其他步骤就能进行额外参数的提取了。}
第三,以定时主动上报为例,我们需要修改PreMain类,进行定时任务的设置
private static final ScheduledThreadPoolExecutor timerExec = new ScheduledThreadPoolExecutor(1);public static void premain(final String options, final Instrumentation inst)throws Exception {AgentOptions agentOptions;try {agentOptions = new AgentOptions(options);} catch (Exception e) {// 如果解析AgentOptions出现异常的时候,不对应用进行插桩,方便对应用进行控制。e.printStackTrace();return;}final Agent agent = Agent.getInstance(agentOptions);final IRuntime runtime = createRuntime(inst);runtime.startup(agent.getData());// 开启定时任务,NETWORKPropsHelper.post 在步骤一中进行了定义;int delay =10L;timerExec.scheduleAtFixedRate(() -> NETWORKPropsHelper.post(agent.getData().getStore()), delay, delay, TimeUnit.MINUTES);inst.addTransformer(new CoverageTransformer(runtime, agentOptions,IExceptionLogger.SYSTEM_ERR));}
第四,最终覆盖率数据的序列化,因为覆盖率是整个应用工程的,所以需要序列化的内容包括,包,类,方法,行等,非常的多。以下以2个核心接口为例
public interface ICoverageNode extends Serializable {...default String toJSONString() {return "{\"" + CONSTANTS.ELEMENT_TYPE + "\":\"" + getElementType().name() + "\"," +"\"" + CONSTANTS.ELEMENT_NAME + "\":\"" + getName() + "\"," +"\"" + CONSTANTS.METRICS_INSTRUCTION + "\":" + getInstructionCounter().toJSONString() + "," +"\"" + CONSTANTS.METRICS_BRANCH + "\":" + getBranchCounter().toJSONString() + "," +"\"" + CONSTANTS.METRICS_LINE + "\":" + getLineCounter().toJSONString() + "," +"\"" + CONSTANTS.METRICS_METHOD + "\":" + getMethodCounter().toJSONString() + "," +"\"" + CONSTANTS.METRICS_CLASS + "\":" + getClassCounter().toJSONString() + "," +"\"" + CONSTANTS.METRICS_COMPLEX + "\":" + getInstructionCounter().toJSONString() +"}";}}public interface ICounter extends Serializable {...default String toJSONString() {return "{\"" + CONSTANTS.TOTAL + "\":" + getTotalCount() + "," +"\"" + CONSTANTS.COVERED + "\":" + getCoveredCount() + "}";}}public class CONSTANTS {// 所有内容转化成json后的key 的label信息public static final String CLASSES = "classes";public static final String METHOD_DESC = "desc";public static final String METHOD_SIGNATURE = "signature";public static final String METHOD_LINES = "lines";public static final String LINE_NBR = "nbr";public static final String LINE_STATUS = "status";public static final String METHODS = "methods";public static final String PACKAGES = "packages";public static final String ELEMENT_TYPE = "elementType";public static final String ELEMENT_NAME = "name";public static final String METRICS_INSTRUCTION = "instruction";public static final String METRICS_BRANCH = "branch";public static final String METRICS_LINE = "line";public static final String METRICS_METHOD = "method";public static final String METRICS_CLASS = "class";public static final String METRICS_COMPLEX = "complex";public static final String TOTAL = "total";public static final String COVERED = "covered";}
改动涉及到的类:

最后一步,技术含量一般,但是工作量比较大,结果需要由bundleCoverage统一执行序列化,进行最后覆盖率数据的json字符串的生成。这样生成出来的字符串对象会特别大,大的会超过10M,所以才有了第一步的压缩byte数组操作。
有这了种流程,我们就可以更低成本的接入代码覆盖率,而且在不同的发布方式下,兼容性也比较可控。当然了,也不是完全没有坏处,因为覆盖率的数据解析全部由应用上的agent承担,因此会对应用本身产生额外的开销,这就需要进行应用评估后才能选择更好的方式。覆盖率本身不需要每秒都进行解析,每10~30分钟一次,每次产生10M数据,一般情况下都不会对应用产生太大的负担。
最后
软件的发展,都是一点点变化发展的,其实就是为了解决某个问题引入了新的解决方案,在某一个时刻得到平衡,之后又因为新需求,再有新的方案进行解决,如此循环往复,测试效能工具也是如此,没有银弹,需要跟随需求一起进化。

更多推荐



所有评论(0)