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文件路径信息,通过路径,获取文件内容,再结合行号出的报告。
因为代码获取方式各异,这里就不说明了,如果有兴趣,可以入群一起讨论。

posted @ 2023-01-18 15:16  月色深潭  阅读(712)  评论(0)    收藏  举报