【Jacoco】多服务exec文件采集、合并、下载实现

服务关系

代码覆盖率数据采集服务
负责和提供数据的目标服务交互,目标服务启动时,指定jacoco-server的相关参数,server端会为每个目标服务启动一个socket线程,持续接收数据流,并形成exec文件,保存至服务器硬盘。
jacoco文件下载服务
负责和jacoco-client交互,建立socket连接,将服务器硬盘中的所有exec文件合并并写入流中。
jacoco相关服务关系图
jacoco相关服务关系图

代码

服务端

  • 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>io.thelen.jacoco.example</groupId>
    <artifactId>citylife-jacoco-tcpserver</artifactId>
    <version>1.0-SNAPSHOT</version>
	<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.jacoco.examples.JacocoServer</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.core</artifactId>
            <version>0.7.9</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>commons-cli</groupId>
            <artifactId>commons-cli</artifactId>
            <version>1.4</version>
        </dependency>
    </dependencies>
</project>
  • Lock工具类
package org.jacoco.utils;

import java.io.Closeable;
import java.util.concurrent.locks.Lock;

/**
 * 自动锁工具类
 * 
 * @author yongsong.li
 */
public class AutoUnlockUtils implements Closeable {

  private final Lock lock;

  public AutoUnlockUtils(Lock lock) {
    this.lock = lock;
  }

  public void lock() {
    lock.lock();
  }

  @Override
  public void close() {
    lock.unlock();
  }
}
  • JacocoServer 主函数
package org.jacoco.examples;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

/**
 * Jacoco-tcp-server
 *  * @author yongsong.li
 */
public class JacocoServer {

  public static void main(String[] args) throws ParseException {
    // 创建commons-option参数映射表
    CommandLineParser commandLineParser = new DefaultParser();
    Options options = new Options();
    options.addOption("filepath", true, "jacoco文件存放路径");
    options.addOption("address", true, "socket主机地址");
    options.addOption("dataport", true, "数据采集服务端口号");
    options.addOption("downloadport", true, "文件下载服务端口号");
    options.addOption("filemaxnum", true, "数据采集服务文件数最大阈值");
    CommandLine commandLine = commandLineParser.parse(options, args);
    // 获取服务参数
    String filepath = commandLine.getOptionValue("filepath");
    String address = commandLine.getOptionValue("address");
    String dataPort = commandLine.getOptionValue("dataport");
    String downLoadPort = commandLine.getOptionValue("downloadport");
    String fileMaxNum = commandLine.getOptionValue("filemaxnum");
    // 开启代码覆盖率数据采集服务
    new ExecuteDataServer(filepath, address, dataPort, fileMaxNum).execute();
    // 开启jacoco文件下载服务
    new DownLoadDataServer(filepath, address, downLoadPort).execute();
  }
}
  • Jacoco服务抽象类
package org.jacoco.examples;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Jacoco服务抽象类
 *  * @author yongsong.li
 */
public abstract class AbstractServer {

  /**
 * 获取ServerSocket
 * 
 * @return {@link ServerSocket}
 * @throws IOException IOException
   */
  abstract ServerSocket getServer() throws IOException;

  /**
 * 获取Handler
 * 
 * @param server server
 * @return {@link Runnable}
   */
  abstract Runnable getHandler(Socket server);

  /**
 * 监听server端口逻辑
 * 
 * @throws IOException IOException
   */
  private void doHandler() throws IOException {
    // create server
    ServerSocket server = getServer();
    // 开启监听
    while (true) {
      Runnable handler = getHandler(server.accept());
      // create Thread
      new Thread(handler).start();
    }
  }

  /**
 * 创建新线程执行doHandler方法,防止单个server中accept方法阻塞
   */
  public void execute() {
    new Thread(() -> {
      try {
        doHandler();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }).start();
  }
}
  • 代码覆盖率数据采集服务
package org.jacoco.examples;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

import org.apache.commons.io.FileUtils;
import org.jacoco.core.data.ExecutionData;
import org.jacoco.core.data.ExecutionDataWriter;
import org.jacoco.core.data.IExecutionDataVisitor;
import org.jacoco.core.data.ISessionInfoVisitor;
import org.jacoco.core.data.SessionInfo;
import org.jacoco.core.runtime.RemoteControlReader;
import org.jacoco.core.runtime.RemoteControlWriter;

/**
 * 代码覆盖率数据采集服务
 *  * @author yongsong.li
 */
public class ExecuteDataServer extends AbstractServer {

  private static String FILEPATH = "/application/software/jacoco/";

  private static String ADDRESS = "0.0.0.0";

  private static int PORT = 6300;

  private static int FILEMAXNUM = 2000;

  private static final String EXEC = ".exec";

  ExecuteDataServer(String filePath, String address, String port, String fileMaxNum) {
    if (filePath != null && !filePath.equals("")) {
      FILEPATH = filePath;
    }
    if (address != null && !address.equals("")) {
      ADDRESS = address;
    }
    if (port != null && !port.equals("")) {
      PORT = Integer.parseInt(port);
    }
    if (fileMaxNum != null && !fileMaxNum.equals("")) {
      FILEMAXNUM = Integer.parseInt(fileMaxNum);
    }
  }

  @Override
  ServerSocket getServer() throws IOException {
    return new ServerSocket(PORT, 0, InetAddress.getByName(ADDRESS));
  }

  @Override
  Runnable getHandler(Socket server) {
    try {
      return new Handler(server);
    } catch (IOException e) {
      e.printStackTrace();
    }
    return null;
  }

  private class Handler implements Runnable, ISessionInfoVisitor, IExecutionDataVisitor {

    /**
     * socket
     */
    private final Socket socket;

    /**
     * RemoteControlReader
     */
    private final RemoteControlReader reader;

    /**
     * ExecutionDataWriter
     */
    private ExecutionDataWriter fileWriter;

    /**
     * RemoteControlWriter
     */
    private final RemoteControlWriter remoteWriter;

    Handler(final Socket socket) throws IOException {
      this.socket = socket;
      remoteWriter = new RemoteControlWriter(socket.getOutputStream());
      reader = new RemoteControlReader(socket.getInputStream());
      reader.setRemoteCommandVisitor(remoteWriter);
      reader.setSessionInfoVisitor(this);
      reader.setExecutionDataVisitor(this);
    }

    // execute
    public void run() {
      // 定义文件对象
      File file = null;
      // try-resource
      try (socket) {
        // while循环读取socket的input
        while (true) {
          // 获取文件名称集合
          File[] files = new File(FILEPATH).listFiles();
          // 判断文件数是否超过阈值
          if (files != null && files.length > FILEMAXNUM) {
            // 等待下一次循环
            Thread.sleep(10000);
            continue;
          }
          // 创建exec文件保存路径
          String path = FILEPATH + UUID.randomUUID();
          file = new File(path);
          // 获取文件输出流
          // try-resource
          try (FileOutputStream outputStream = FileUtils.openOutputStream(file)) {
            System.out.println("open" + path);
            // 更新输出流到ExecutionDataWriter
            fileWriter = new ExecutionDataWriter(outputStream);
            remoteWriter.visitDumpCommand(true, false);
            // 判断是否读取完毕
            if (!reader.read()) {
              break;
            }
            // 写入输出流
            fileWriter.flush();
          }
          System.out.println("close" + path);
          // 修改文件名称,加.exec后缀名
          file.renameTo(new File(path + EXEC));
          // 等待下一次循环
          Thread.sleep(300000);
        }
      } catch (final IOException | InterruptedException e) {
        if (file != null)
          file.delete();
        e.printStackTrace();
      }
    }

    public void visitSessionInfo(final SessionInfo info) {
      System.out.printf("Retrieving execution Data for session: %s%n", info.getId());
      fileWriter.visitSessionInfo(info);
    }

    public void visitClassExecution(final ExecutionData data) {
      fileWriter.visitClassExecution(data);
    }
  }
}
  • jacoco文件下载服务
package org.jacoco.examples;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;

import org.jacoco.core.tools.ExecFileLoader;
import org.jacoco.utils.AutoUnlockUtils;

/**
 * jacoco文件下载服务
 *  * @author yongsong.li
 */
public class DownLoadDataServer extends AbstractServer {

  private static String FILEPATH = "/application/software/jacoco/";

  private static String ADDRESS = "0.0.0.0";

  private static int PORT = 6301;

  private static final String EXEC = ".exec";

  DownLoadDataServer(String filePath, String address, String port) {
    if (filePath != null && !filePath.equals("")) {
      FILEPATH = filePath;
    }
    if (address != null && !address.equals("")) {
      ADDRESS = address;
    }
    if (port != null && !port.equals("")) {
      PORT = Integer.parseInt(port);
    }
  }

  @Override
  ServerSocket getServer() throws IOException {
    return new ServerSocket(PORT, 0, InetAddress.getByName(ADDRESS));
  }

  @Override
  Runnable getHandler(Socket server) {
    return new Handler(server);
  }

  private static class Handler implements Runnable {

    /**
     * socket
     */
    private final Socket socket;

    /**
     * static:线程同步使用
     */
    private static final ReentrantLock lock = new ReentrantLock();

    Handler(final Socket socket) {
      this.socket = socket;
    }

    // execute
    public void run() {
      try (socket; AutoUnlockUtils autoUnlockUtils = new AutoUnlockUtils(lock)) {
        // lock
        autoUnlockUtils.lock();
        // 获取文件夹
        File mergeDir = new File(FILEPATH);
        // 获取文件名称集合
        File[] files = mergeDir.listFiles();
        // 判断是否有文件
        if (files == null || files.length == 0) {
          return;
        }
        // 取指定后缀名为.exec的文件
        files = Stream.of(files).filter(file -> file.getAbsolutePath().endsWith(EXEC)).toArray(File[]::new);
        // create loader
        ExecFileLoader loader = new ExecFileLoader();
        // 循环读取文件并写入到输出流
        for (File file : files) {
          loader.load(file);
          loader.save(socket.getOutputStream());
        }
        // 删除文件
        for (File file : files) {
          file.delete();
        }
      } catch (final IOException e) {
        e.printStackTrace();
      }
    }
  }
}

客户端

  • 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>io.thelen.jacoco.example</groupId>
    <artifactId>citylife-jacoco-tcpclient</artifactId>
    <version>1.0-SNAPSHOT</version>
	<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.jacoco.examples.JacocoClient</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>commons-cli</groupId>
            <artifactId>commons-cli</artifactId>
            <version>1.4</version>
        </dependency>
    </dependencies>

</project>
  • JacocoClient 主函数
package org.jacoco.examples;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Jacoco-tcp-client
 *  * @author yongsong.li
 */
public class JacocoClient {

    private static String ADDRESS = "0.0.0.0";

    private static int PORT = 6301;

    private static String SAVE_PATH = "/application/software/jacoco-down/";

    private static final String EXEC = ".exec";

    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyyMMddHHmmssSSS");

    public static void main(String[] args) throws ParseException, IOException {
        // 初始化参数
        init(args);
        System.out.println("jacoco-client init");
        //创建保存的文件
        File file = new File(SAVE_PATH + FORMATTER.format(new Date()) + EXEC);
        // 连接server并获取输入流
        try (Socket socket = new Socket(ADDRESS, PORT); InputStream is = socket.getInputStream()) {
            // 流复制到文件
            FileUtils.copyInputStreamToFile(is, file);
            //文件大小为空,删除
            if (FileUtils.sizeOf(file) == 0) {
                FileUtils.forceDeleteOnExit(file);
            }
            System.out.println("jacoco-client end");
        } catch (Exception e) {
            FileUtils.forceDeleteOnExit(file);
            e.printStackTrace();
        }
    }

    /**
     * 初始化参数
     */
    private static void init(String[] args) throws ParseException {
        // 创建commons-option参数映射表
        CommandLineParser commandLineParser = new DefaultParser();
        Options options = new Options();
        options.addOption("savepath", true, "jacoco文件下载路径");
        options.addOption("address", true, "下载服务-socket主机地址");
        options.addOption("port", true, "下载服务-socket端口号");
        CommandLine commandLine = commandLineParser.parse(options, args);
        // 获取服务参数
        String savePath = commandLine.getOptionValue("savepath");
        String address = commandLine.getOptionValue("address");
        String port = commandLine.getOptionValue("port");
        if (savePath != null && !savePath.equals("")) {
            SAVE_PATH = savePath;
        }
        if (address != null && !address.equals("")) {
            ADDRESS = address;
        }
        if (port != null && !port.equals("")) {
            PORT = Integer.parseInt(port);
        }
    }
}

启动参数

  • 服务端
Program arguments:
-filepath E:\jacoco\ -address 127.0.0.1 -dataport 6300 -downloadport 6301 -filemaxnum 2000
  • 客户端
Program arguments:
-savepath E:\jacoco-down\ -address 127.0.0.1 -port 6301
  • 目标服务
VM options:
-javaagent:D:/jacoco/lib/jacocoagent.jar=includes=com.*,output=tcpclient,port=6300,address=127.0.0.1,sessionid=temp,append=true

注:

  1. 要先启动服务端,再启动目标服务或客户端。
  2. javaagent指向的是jacocoagent.jar,可以自己去官网下载

采集结果示例

采集结果示例

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值