开发自救手册 开发自救手册
首页
  • Java指南

    • 手册的初衷以及适用人群
    • 介绍
    • JAVA涉及技术总览
  • 技术场景

    • 线上代码覆盖率监控
  • cpp自救指南
  • go指南
  • 真正的面经系列
  • 经历分享
  • 相关文章

    • 25届实习内推
  • 微服务
友情链接
  • 团队介绍
  • 如何加入
GitHub (opens new window)
首页
  • Java指南

    • 手册的初衷以及适用人群
    • 介绍
    • JAVA涉及技术总览
  • 技术场景

    • 线上代码覆盖率监控
  • cpp自救指南
  • go指南
  • 真正的面经系列
  • 经历分享
  • 相关文章

    • 25届实习内推
  • 微服务
友情链接
  • 团队介绍
  • 如何加入
GitHub (opens new window)
  • 指南

    • 手册的初衷以及适用人群
    • 介绍
    • JAVA涉及技术总览
  • 技术场景

    • jacoco线上代码覆盖率分析
      • 背景
        • 调研
        • 理论
        • 实践
        • 启动
        • 导出exec并分析
        • 分析
        • 总结
    • vuepress搭建个人博客
  • 开发认知扩展
  • 技术场景
July
2024-04-26
目录

jacoco线上代码覆盖率分析

# 背景

代码覆盖率,就是衡量代码分支被运行到的比例,所以被称作覆盖率。

大家都知道,jacoco可以分析单测分支覆盖率,例如用Idea可以统计单测覆盖率。 但是在线上运行的核心链路中,代码沉淀好几年,俗称shi山代码,又如何对线上代码影响最小的方式,统计线上覆盖率并重构。

# 调研

Jacoco插桩原理简介:利用Javaagent的修改字节码技术,每个类分配一个Boolean数组,在一些分支,方法中插入boolean修改值,这样走到该方法或者分支上的Boolean值就是true,通过这种方式可以分析出是否覆盖。
该插桩对代码影响可以说非常小,对接口rt也基本不会有影响,可以选择jacoco做覆盖率分析,对线上应用影响可以忽略。

# 理论

Jacoco agent有两种模式

  • 模式一:Offline模式,提前对jar包做修改,运行中生成覆盖率信息到文件中,适合单测使用
  • 模式二:On-the-fly,用javaagent进行动态修改字节码,在程序关闭后自动生成一个exec文件
  • 模式三:On-the-fly tcpClient模式,需要自己搭建一个jacocoServer,显然成本太高,并且不同环境隔离的问题比较麻烦
  • 模式四:On-the-fly tcpServer模式,agent会启动一个tcp服务,需要的时候用jacocoClient从该端口拉当前覆盖率

image.png

线上应用不能随便停机,而且现在很多应用都是容器化,停后文件会丢失,那么显而易见,模式四完美解决了这个问题和我们的需求,那么我们只要将代码放到线上跑7天,然后进入机器用jacocoClient dump下来jacoco.exec文件,实时分析。

# 实践

理论可行,实践开始。
下载jacoco,官网下载 (opens new window)随便选择一个,我这里选择0.8.8,切记agent和client要版本一致就行。

image.png
解压后进入lib目录下,可以看见下图,我们会用到jacocoagent.jar和jacococli.jar,解压出来

image.png
我们先创建一个SpringBoot项目,普通Java项目也可以通过这种方式agent

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>org.example</groupId>
    <artifactId>JacocoSpringBoot</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.6.13</version>
        </dependency>
    </dependencies>
</project>

Application

package com.zhusheng.jacoco;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

TestController

package com.zhusheng.jacoco.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;

@RestController
public class TestController {
    
    private List<String> list = Collections.emptyList();
    
    @GetMapping("/test")
    public String getTest(@RequestParam("id") Long id) {
        if (id == 1) {
            System.out.println("走到这里");
        } else {
            System.out.println("走到下面");
        }
        return id.toString();
    }
    
    public String test() {
        List<String> list1 = list;
        System.out.println(1);
        return "没走到这里";
    }
    
}

Config

package com.zhusheng.jacoco.config;


public class Config {
    
    public Config() {
        int a = 10;
        int b = 20;
        System.out.println("走到这里");
    }
}

TestConfiguration

package com.zhusheng.jacoco.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class TestConfiguration {
    
    @Bean
    public Config config() {
        System.out.println("走到这里");
        return new Config();
    }
}

# 启动

添加agent参数

-javaagent:D:\openSource\jacocoagent.jar=includes=com.zhusheng.jacoco.*,output=tcpserver,port=2024,address=127.0.0.1

参数含义

D:\openSource\jacocoagent.jar是你解压jacocoagent的绝对路径
includes=com.zhusheng.jacoco.*表示只对com.zhusheng.jacoco包下做插桩
output=tcpserver以tpcServer模式启动
port=2024表示tcp端口2024
address=127.0.0.1可访问ip

访问一下 http://localhost:8080/test?id=1 测试效果 用lsof可以看下2024端口是否被监听成功

# 导出exec并分析

来到jacococli的目录下

java -jar jacococli.jar dump --address localhost --port 2024 --destfile ./jacoco.exec

可以测试一下是否agent成功

java -jar jacococli.jar execinfo jacoco.exec

可以看到,jacoco已经成功插桩并且在项目稳定运行中可以实时统计出覆盖率。

# 分析

这里推荐直接使用idea进行分析
RUN->Show Coverage Data...选择刚刚dump下来的jacoco.exec image.png

image.png

左侧为是否hit到该行 image.png
覆盖率 image.png
类覆盖率,方法覆盖率,行覆盖率 image.png
注意:如果发现覆盖率都是0,则是jacoco.exec中的classId跟你目前的classId匹配不上,在idea中重新build一下并且确保本地代码跟线上代码是一致的,否则会出现覆盖率为0的问题。
拿到行覆盖率后,就可以愉快的删除shi山代码以及重构了,再也不怕因为不知道这行代码用不用的上而不敢删别人的垃圾代码了。

# 总结

jacoco能以一种侵入非常低的方式,并且不需要线上停机的方式统计出线上真实的代码覆盖率。有想更深入了解jacoco原理的可以去学习下,jacoco是统计覆盖率最优解也是相对完美的解决方案了,本文就不做太多扩展。

上次更新: 2024/04/28, 17:23:46
JAVA涉及技术总览
vuepress搭建个人博客

← JAVA涉及技术总览 vuepress搭建个人博客→

Theme by Vdoing | Copyright © 2019-2024 Backend Development | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式