9. jvm-sandbox之代码覆盖
代码覆盖
概述
JVM-Sandbox的通知监听器中,有一个beforeLine方法,可以提供方法中,被执行过的代码行号,通过收集这些行号,来实现代码覆盖功能。
/**
* 行为即将经过的代码行
*
* @param advice Caller的行为通知
* @param lineNum 即将经过的代码行
*/
protected void beforeLine(Advice advice, int lineNum) {
}
模块实现
@MetaInfServices(Module.class)
@Information(id = "code-coverage", version = "0.0.1", author = "hch")
public class CodeCoverageModule extends ParamSupported implements Module {
private final Logger lifeCLogger = LoggerFactory.getLogger("CODE-COVERAGE-MODULE");
@Resource
private ModuleEventWatcher moduleEventWatcher;
private ExecutorService executor = Executors.newCachedThreadPool();
private int watchId = 0;
@Command("codecoverage")
public void codecoverage(final Map<String, String> param,final PrintWriter writer) {
//下面是获取启动功能时,传入的代码分支,以及服务名称和测试环境信息。其中服务名称和测试环境信息,和服务部署有关,需要按实际情况而定。
final String codeCoverage_branch=getParameter(param,"codeCoverage_branch","master");
String serviceName = CustomSystemUtil.getProjectName().replace("-", "_");
String env = CustomSystemUtil.getTestEnvName();
lifeCLogger.debug("env: {} , serviceName: {}", env, serviceName);
JSONObject jsonObject = new JSONObject();
jsonObject.fluentPut("serviceName", serviceName);
if (watchId == 0) {
try {
createWatcher(env, serviceName,codeCoverage_branch);//创建watcher
watchId = 100;
jsonObject.fluentPut("result", "ok");
} catch (Exception e) {
jsonObject.fluentPut("result", "error");
}
} else {
jsonObject.fluentPut("result", "already exist");
}
writer.println(jsonObject.toJSONString());
writer.flush();
}
private void createWatcher(String env, String serviceName,String codeCoverage_branch) throws Exception {
//新建一个AdviceListener
AdviceListener adviceListener = new AdviceListener() {
@Override
protected void before(Advice advice) throws Throwable {
if (advice.isProcessTop()) {//如果递进调用过程中的顶层通知,就在attachment中新增一个map,用于存放后续代码行信息
Map<String, Object> map = new HashMap<>();
map.put("threadClassAndMethodInfos", new ArrayList<JSONObject>());
advice.attach(map);
}
}
@Override
protected void after(Advice advice) throws Throwable {
if (advice.isProcessTop()) {
Map<String, Object> map = advice.attachment();
List<JSONObject> list = (List<JSONObject>) map.get("threadClassAndMethodInfos");
sendMessage(list);//调用结束后,将收集到的代码行信息上传到服务器中
}
}
@Override
protected void beforeLine(Advice advice, int lineNum) {
Map<String, Object> map = advice.getProcessTop().attachment();
if (advice.isProcessTop()) {
map.put("entranceLineNum", lineNum);//这里记录一下,递进调用过程中的顶层入口行号,备用
}
beforeLineHandle(advice, lineNum, env, serviceName,codeCoverage_branch);//最重要的处理过程
}
};
executor.submit(new Runnable() {
@Override
public void run() {
CodeCoverageProcess codeCoverageProcess = new CodeCoverageProcess();
new EventWatchBuilder(moduleEventWatcher, EventWatchBuilder.PatternType.REGEX)//一定要选择这种表达式模式
.onClass(bulidClassPattern())//设置类的正则表达式
.onAnyBehavior()
.onWatching()
.withLine()//有它,才能获取到行号
.withProgress(codeCoverageProcess)//可以不用Process,我这里用它查看是否已经渲染完成,没有其它作用
.onWatch(adviceListener);
}
});
}
private void beforeLineHandle(Advice advice, int lineNum, String env, String serviceName,String codeCoverage_branch) {
//提取attachment信息
Map<String, Object> map = advice.getProcessTop().attachment();
//线程调用链路中的类和方法信息
List<JSONObject> threadClassAndMethodInfos = (List<JSONObject>) map.get("threadClassAndMethodInfos");
//线程入口 行号
int entranceLineNum = (int) map.get("entranceLineNum");
//线程入口信息
String entrance = advice.getProcessTop().getTarget().getClass().getName() + "::" + advice.getProcessTop().getBehavior().getName() + "::" + entranceLineNum;
//当前触发事件信息
String currentBehavior = advice.getTarget().getClass().getName() + "::" + advice.getBehavior().getName();
//方法名称
String methodName = advice.getBehavior().getName();
//获取实现方法的具体父类名称
String className = queryClassName(advice.getTarget().getClass(), methodName);
if("null".equals(className)){
className=advice.getTarget().getClass().getName();//兜底返回当前类名称
}
if (className.contains("$")) {//调用异步方法的类名,会带$符号,目前用一种简陋的方法解决
className = className.split("\\$")[0];
}
//组装准备发送到服务器的的消息,注意这里只是单条消息,真正发送到服务的是批量信息,是threadClassAndMethodInfos这个list。在after事件时发送。
JSONObject jsonObject = new JSONObject();
jsonObject.fluentPut("env", env);
jsonObject.fluentPut("serviceName", serviceName);
jsonObject.fluentPut("className", className);
jsonObject.fluentPut("lineNum", lineNum);
jsonObject.fluentPut("event", "beforeLine");
jsonObject.fluentPut("entrance", entrance + " => " + currentBehavior);
jsonObject.fluentPut("parameters", advice.getParameterArray());
jsonObject.fluentPut("codeCoverage_branch",codeCoverage_branch);
//将单条消息添加到threadClassAndMethodInfos
threadClassAndMethodInfos.add(jsonObject);
}
//根据实际情况 构建匹配类的正则表达式
private String bulidClassPattern() {
//com(?!\.logger\.|\.frame\.|.*\.dto\.|.*\.constants\.|.*\.model\.).*
StringBuffer sb = new StringBuffer("com(?!");
//common
sb.append("\\.logger\\.").append("|")
.append("\\.frame\\.").append("|")
.append("\\.idgenerator\\.").append("|")
.append("\\.pigeonV2\\.").append("|")
.append("\\.liteflow\\.").append("|")
.append(".*\\.constant\\.");
sb.append(").*");
return sb.toString();
}
//after事件后的批量发送 处理后的覆盖行信息
private void sendMessage(List<JSONObject> list) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json; charset=utf-8");
String url = "http://-----/jvmsandbox/codeCoverageInfos";
try {
HttpUtil.Resp resp = HttpUtil.invokePostBody(url, headers, JSONObject.toJSONString(list));
int code = resp.getCode();
if (code != 200) {
lifeCLogger.debug("发送失败:{}", code);
}
} catch (Exception e) {
lifeCLogger.debug("发送异常:{}", e.getMessage());
}
}
//校验方法是否当前类实现,如果不是,那么逐层向上获取父类,并验证是否是父类实现
private String queryClassName(Class type, String methodName) {
String className = null;
Method[] methods = type.getDeclaredMethods();//获取当前类实现的方法(getMethods()是获取所有方法,包括抽象)
for (Method method : methods) {
if (methodName.equals(method.getName())) {
className = type.getName();
}
}
if (className == null) {
Class superClass = type.getSuperclass();
if (superClass.getName().contains("java.lang.Object")) {
return "null";
} else {
className = queryClassName(superClass, methodName);
}
}
return className;
}
}
后续
拿到代码行的信息后,还需要获取代码信息,才能得到方法的覆盖报告。我这边是通过github提供的API,遍历出服务所有Java文件路径信息,通过路径,获取文件内容,再结合行号出的报告。
因为代码获取方式各异,这里就不说明了,如果有兴趣,可以入群一起讨论。
本文来自博客园,作者:月色深潭,交流群:733423266,转载请注明原文链接:https://www.cnblogs.com/moonpool/articles/17059739.html

浙公网安备 33010602011771号