7. jvm-sandbox之服务接口扫描

服务接口扫描

一、概述

前段时间在测试环境部署了jvm-sandbox-repeater,成功录制到请求记录。鉴于项目中出现过业务漏测的情况(有服务的新接口未覆盖到),所以想实现一个接口覆盖的功能。
主要原理是通过对比录制的接口记录和扫描到的服务接口差异,从而知道在项目测试时间段内,哪些接口没有被覆盖到。

二、编写Module

2.1 获取SpringClassLoad和controller类的路径

这里当sandbox被加载完成时,新建了两个EventWatchBuilder。
一个针对refresh方法,获取到spring的加载类
另一个针对buildDefaultBeanName方法,获取到带“controller”的类路径名称集合
代码如下:

@Override
    public void loadCompleted() {
         new EventWatchBuilder(moduleEventWatcher)
                .onClass("org.springframework.context.support.AbstractApplicationContext")
                .onBehavior("refresh")
                .onWatching()
                .withCall()
                .onWatch(new AdviceListener() {
                    @Override
                    protected void before(Advice advice) throws Throwable {
                        if (springClassLoadIsNull()) {
                            setSpringClassLoad(advice.getBehavior().getDeclaringClass().getClassLoader());
                        }
                    }
                });
        new EventWatchBuilder(moduleEventWatcher)
                .onClass("org.springframework.context.annotation.AnnotationBeanNameGenerator")
                .onBehavior("buildDefaultBeanName")
                .onWatching()
                .withCall()
                .onWatch(new AdviceListener() {
                    @Override
                    protected void before(Advice advice) throws Throwable {
                        Object o=advice.getParameterArray()[0];//BeanDefinition
                        IBeanDefinition beanDefinition = InterfaceProxyUtils.puppet(IBeanDefinition.class, o);
                        String s = beanDefinition.getBeanClassName();
                        if(StringUtils.containsIgnoreCase(s,"controller")){
                            if(!controllerPackageNames.contains(s)){
                                lifeCLogger.debug("===="+s);
                                controllerPackageNames.add(s);
                            }
                        }
                    }
                });
    }

2.2 获取类路径名称集合

这里编写了一个Command,给调用服务返回类路径名称集合

 @Command("controllerPackageNames")
    public void controllers(final PrintWriter writer){
        lifeCLogger.debug("controllerPackageNames");
        writer.println(JSONObject.toJSONString(controllerPackageNames));
        writer.flush();
    }

2.3 组建接口信息

有了加载类和类路径后,就可以通过反射,在controller类对象中拿到所有注解信息,再做一些处理,就可以得到完整的接口信息了。

2.3.1 编写一个Command

这里编写了一个处理单个controller类的Command,具体遍历controller类路径名称集合的逻辑在另外的调用服务中实现。

 @Command("uris")
    public void uris(final Map<String, String> param, final PrintWriter writer) {
        final String appName = getParameter(param, "appName");
        final String env = getParameter(param, "env");
        final String packageName = getParameter(param, "packageName");
        final String version = getParameter(param, "version");
        if (springClassLoadIsNull()) {
            lifeCLogger.debug("还没有获取到SpringClassLoad");
            writer.println("error: SpringClassLoad is null");
        } else {
            if (null == packageName && null == appName && null == env) {
                lifeCLogger.debug("appName,env,packageName 有一个参数为空");
                writer.println("error: one of params(appName env packageName) is null");
            } else {
                lifeCLogger.debug("当前spring类加载器 SpringClassLoad :{}", this.springClassLoad.getClass().getName());
                lifeCLogger.debug("generate uris for {} - {} begin", env, appName);
                try {
                    Class<?> targetClass = this.springClassLoad.loadClass(packageName);
                    //获取basePath
                    String basePath = takeBasePath(targetClass);
                    //遍历method获取最终的结果
                    List<Map<String, String>> resultList = traversalMethods(targetClass, basePath, appName, env, packageName,version);
                    writer.println(JSONObject.toJSONString(resultList));
                    lifeCLogger.debug("generate uris for {} - {} result: {} ", env, appName, JSONObject.toJSONString(resultList));
                    lifeCLogger.debug("generate uris for {} - {} end ", env, appName);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    lifeCLogger.debug(e.getMessage());
                } catch (Exception e) {
                    e.printStackTrace();
                    lifeCLogger.debug(e.getMessage());
                } finally {
                    writer.flush();
                }
            }

        }

    }

2.3.2 获取Base路径的实现

public String takeBasePath(Class<?> targetClass) {
        Annotation[] annotations=targetClass.getAnnotations();
        for(Annotation annotation:annotations){
            if(annotation.annotationType().getSimpleName().equals("RequestMapping")){
                String[] s=(String[]) InstanceUtils.doMethodByName("value",annotation);
                lifeCLogger.debug(JSONObject.toJSONString(s));
                String basePath=s[0];
                //处理一下basePath  保证格式是/123/456
                if (!basePath.startsWith("/")) {
                    basePath = "/" + basePath;
                }
                if (basePath.endsWith("/")) {
                    basePath = basePath.substring(0, basePath.length() - 1);
                }
                if("/".equals(basePath)){
                    basePath="";
                }
                return basePath;
            }
        }
        return "";
    }

2.3.3 获取子路径实现

 public Map<String, String> handleSubPath(Method method, String basePath, String appName, String env, String packageName,String version) {
        Map<String, String> resultMap = new HashMap<>();

        Annotation[] annotations = method.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().getSimpleName().equals("RequestMapping")) {
                resultMap.put("appName", appName);
                resultMap.put("env", env);
                resultMap.put("packageName", packageName);
                resultMap.put("version",version);
                String[] values = (String[]) InstanceUtils.doMethodByName("value", annotation);
                String path = values[0];
                if(null!=path){
                    if(!path.startsWith("/")){
                        path="/"+path;
                    }
                    if(path.endsWith("/")){
                        path = path.substring(0, path.length() - 1);
                    }
                    if("/".equals(path)){
                        resultMap.put("uri", basePath);
                    }else{
                        resultMap.put("uri", basePath + path);
                    }
                }else{
                    resultMap.put("uri", basePath);
                }
                Object requestMethods_object = InstanceUtils.doMethodByName("method", annotation);
                String re = JSONObject.toJSONString(requestMethods_object);
                List<RequestMethod> requestMethods = JSONObject.parseArray(re, RequestMethod.class);
                StringBuffer methodString = new StringBuffer();
                if (requestMethods.size() > 0) {//处理method格式
                    for (int i = 0; i < requestMethods.size(); i++) {
                        methodString.append(requestMethods.get(i));
                        if (i != requestMethods.size() - 1) {
                            methodString.append(",");
                        }
                    }
                }
                resultMap.put("method", methodString.length() > 0 ? methodString.toString() : "default");
            }
            if (annotation.annotationType().getSimpleName().equals("PostMapping")) {
                resultMap.put("appName", appName);
                resultMap.put("env", env);
                resultMap.put("packageName", packageName);
                resultMap.put("version",version);
                resultMap.put("method", "post");
                String[] values = (String[]) InstanceUtils.doMethodByName("value", annotation);
                String path = values[0];
                if (null != path && !path.startsWith("/")) {
                    resultMap.put("uri", basePath + "/" + path);
                } else {
                    resultMap.put("uri", basePath + path);
                }
            }

            if (annotation.annotationType().getSimpleName().equals("GetMapping")) {
                resultMap.put("appName", appName);
                resultMap.put("env", env);
                resultMap.put("packageName", packageName);
                resultMap.put("version",version);
                resultMap.put("method", "get");
                String[] values = (String[]) InstanceUtils.doMethodByName("value", annotation);
                String path = values[0];
                if (null != path && !path.startsWith("/")) {
                    resultMap.put("uri", basePath + "/" + path);
                } else {
                    resultMap.put("uri", basePath + path);
                }
            }
        }
        return resultMap;
    }

三、结果

3.1接口信息

接口信息

3.2 接口覆盖情况

这里利用钉钉群消息功能,将汇总消息发送通知到项目群中
钉钉通知

四、总结

1.  接口覆盖不如代码覆盖精确,但是也可以避免主功能漏测。
2. 通过测试环境中录制的记录,也可以分析出测试了哪些场景。
3. 上面只给出了Module的编写方法,关于调用服务的代码比较简单,这里没有给出。

五、参考

Spring组件注册注解之@ComponentScan,@ComponentScans

posted @ 2021-08-10 11:30  月色深潭  阅读(685)  评论(1编辑  收藏  举报