服务关系
代码覆盖率数据采集服务
负责和提供数据的目标服务交互,目标服务启动时,指定jacoco-server的相关参数,server端会为每个目标服务启动一个socket线程,持续接收数据流,并形成exec文件,保存至服务器硬盘。
jacoco文件下载服务
负责和jacoco-client交互,建立socket连接,将服务器硬盘中的所有exec文件合并并写入流中。
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
注:
- 要先启动服务端,再启动目标服务或客户端。
- javaagent指向的是jacocoagent.jar,可以自己去官网下载