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
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值