如何让EclEmma报告包含自定义ClassLoader加载类的覆盖率?
要解决自定义ClassLoader加载类的覆盖率统计问题,核心是让Jacoco Agent(EclEmma基于它)能拦截到类的加载过程,并复用Agent的运行时环境收集执行数据,具体可按以下步骤操作:
1. 确保Jacoco Agent能拦截自定义ClassLoader的类加载
Jacoco Agent通过java.lang.instrument.Instrumentation的类转换器插装类,默认会处理所有类加载器加载的类,但如果自定义ClassLoader未正确触发转换器,需调整Agent参数并验证:
- 调整VM启动参数,明确指定要覆盖的类,并开启类转储验证插装情况:
-javaagent:/path/to/jacocoagent.jar=includes=your.package.**.,output=tcpclient,port=NNNNN,classdumpdir=jacoco_dumpincludes:指定要统计覆盖率的包或类,例如com.yourcompany.**classdumpdir:将插装后的类文件输出到指定目录,可检查自定义ClassLoader加载的类是否被正确插装
2. 复用Jacoco Agent的Runtime环境,避免创建独立Runtime
你当前代码手动创建了LoggerRuntime和RuntimeData,这与EclEmma Agent的运行时完全独立,导致收集的覆盖率数据无法被Agent捕获。正确做法是获取Agent已初始化的Runtime:
修改Coverage类,替换手动创建的Runtime和Instrumenter:
import org.jacoco.core.runtime.IRuntime; import org.jacoco.core.runtime.RuntimeData; import org.jacoco.core.instr.Instrumenter; import java.lang.reflect.Field; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; public static class Coverage implements AutoCloseable { private final IRuntime runtime; private final Instrumenter instrumenter; private final RuntimeData data; private final Set<String> coverageClassNames; public Coverage(Class<?>... coverageClasses) { try { coverageClassNames = Stream.of(coverageClasses) .map(Class::getName) .collect(Collectors.toUnmodifiableSet()); // 获取Jacoco Agent已初始化的Runtime(替换<version>为实际Jacoco版本,如0.8.10对应internal_0_8_10) Field runtimeField = Class.forName("org.jacoco.agent.rt.internal_<version>.Agent") .getDeclaredField("runtime"); runtimeField.setAccessible(true); this.runtime = (IRuntime) runtimeField.get(null); // 获取Agent的RuntimeData Field dataField = runtime.getClass().getDeclaredField("data"); dataField.setAccessible(true); this.data = (RuntimeData) dataField.get(runtime); this.instrumenter = new Instrumenter(runtime); } catch (Exception e) { throw new RuntimeException("Failed to access Jacoco Agent runtime", e); } } // 其余方法保持不变... }
注意:需将org.jacoco.agent.rt.internal_<version>.Agent中的<version>替换为你使用的Jacoco版本号,可通过查看Jacoco agent jar包内的类结构确认。
3. 调整自定义ClassLoader的类加载逻辑
确保自定义ClassLoader加载类时,让Jacoco Agent的转换器处理字节码。复用Agent的Runtime后,可修改InstrumentingClassLoader的findClass方法:
private class InstrumentingClassLoader extends ClassLoader { @Override public Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] originalBytes = loadClassFile(name); // 用Agent的Instrumenter插装类字节码,数据会自动进入Agent的RuntimeData byte[] instrumentedBytes = instrumenter.instrument(originalBytes, name); return defineClass(name, instrumentedBytes, 0, instrumentedBytes.length); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } // 其余方法保持不变... }
4. 确保Session ID一致(手动发送数据时)
如果需要手动发送执行数据到Agent,必须保证Session ID与Agent一致,可通过反射获取:
// 替换<version>为实际Jacoco版本 Field sessionIdField = Class.forName("org.jacoco.agent.rt.internal_<version>.Agent") .getDeclaredField("sessionId"); sessionIdField.setAccessible(true); String sessionId = (String) sessionIdField.get(null);
使用RemoteControlWriter发送数据时指定该Session ID,确保数据被Agent正确接收合并。
原方案不生效的原因
- 手动创建的
LoggerRuntime是独立运行时,与EclEmma Agent的Runtime无关联,收集的覆盖率数据无法被Agent捕获。 - 自定义ClassLoader加载类时,未让Jacoco Agent的类转换器处理字节码,导致Agent无法跟踪类的执行情况。
- Session ID不匹配会导致Agent拒绝接收手动发送的执行数据。
内容的提问来源于stack exchange,提问作者boot-and-bonnet

