代码覆盖率(前端/后端)

jacoco java方法diff处理过程

2021-01-18  本文已影响0人  sw_saii

之前针对jacoco这块代码覆盖率的增量逻辑的处理只是拿到改动的代码的方法后进行对比jacoco中方法的名称, 如果相同我们就认为这个是改动的方法,但是这里就有一个很大的漏洞,java类里面同名方法其实很多的,也就是我们所说的函数的重载这种情况。所以如果按照这种方式,必定导致一个问题就是有一个方法改动后,另外一个方法也需要要求代码覆盖。所以针对这种情况我们不单单需要判断方法的名称,还需要判断方法的参数才行。

原本以为这个过程其实是比较简单的,但是实际上中间遇到了很多的问题。

试错的过程

我们先举找一个简单的例子来看下如何处理,假设我们的源码内容如下:

public void test(String a, String b) {};

针对java的源码解析我们用的是 eclipse jdt, 通过它解析后我们拿到的参数内容为String a, String b, 直接就是我们源码的内容。

同样的jacoco的visitorMethod中获取到的参数是(Ljava/lang/String;Ljava/lang/String;); 这个还是有点差别的。

看上去还是可以解决的,我们可以这么去处理它:

  1. 针对java parse的参数结果通过逗号分割
  2. 针对jvm方法签名通过;分割后拿到的结果每个再通过/分割获取到最后的类似于String.
  3. 比较java parse的参数个数是否与jvm的的参数个数一致,再比较各个参数 java parse的参数是否contain jvm的方法签名参数。
image

似乎这样子就可以解决我们的问题了, 那我们来验证下吧。

image

验证后发现,有很多没有函数重载的方法,都完全被忽略掉了。我们需要具体分析下到底是什么原因了。

1. java parse参数的多样化

现在很多spring 框架的controller层的参数都是类似于这样子的。

@RequestHeader(value = ENConstant.AUTH_USER_AGENT, required = false, defaultValue = "") String userAgent

如果按照我们刚才的处理方式,通过逗号分隔的话, 这里就有三个参数了。

另外还有

Map<String, Stirng> uidNickNameMap, String b 这样子的匹配也会存在有问题。

所以针对这种干扰行的内容,需要首先先剔除掉才行。。

// @RequestHeader(value = ENConstant.AUTH_USER_AGENT, required = false, defaultValue = "") String userAgent
    // 这样子的数据处理 去掉()中的内容
methodParams = methodParams.replaceAll("\\(.*?\\)", "");
// Map<String,String> uidNickNameMap 类似于这种参数
methodParams = methodParams.replaceAll("\\<.*?\\>", "");

感觉问题已经解决了。

2. jvm 方法签名 参数处理

我们发现就一个这样子的方法,竟然就被判定为了方法的参数不一致:

public void a(int a)

通过jdt获取的参数: int a, 然而 jvm方法签名: I;

这?? 不应该是类似于 java/lang/Integer;吗? 看来问题真没那么简单了。 要了解这块的内容, 我们需要了解一些基本的支持了,就是关于class文件编译以后的方法签名的内容。

JVM 内部使用方法签名与我们日常阅读的方法签名不太一样

字段描述符(Field Descriptor),是一个表示类、实例或局部变量的语法符号,它的表示形式是紧凑的,比如 int 是用 I 表示的。完整的类型描述符如下表

image

比如说:

Object foo(int i, double d, Thread t)的描述符为(IDLjava/lang/Thread;)Ljava/lang/Object;

所以针对这样子的参数 首先我们通过分号分割以后,还需要针对每个分割后的值做处理。循环判断它是否是一个引用类型,即以L开头,如果不是,则是一个基本类型。需要单独提取出来做为一个参数来进行处理。

这里我们是不是遗漏了个什么? 对 最后一个[ 表示这个是一个数组。
类似于我们经常用到的

 public static void main(java.lang.String[]);

它的方法签名就是 ([Ljava/lang/String;)V

所以这个[其实是没有太多意义的,我们可以直接用字符串替换,或者说循环匹配处理的时候忽略到提取到的这个字符

至此感觉基本上都已经完美了。是这样子吗?

3. 内部类

我们继续

package com.seewo;
public class OuterClass {
    private String name ;
    private int age;

    /**省略getter和setter方法**/
    
    public class InnerClass{
        public InnerClass(){
            name = "chenssy";
            age = 23;
        }
        
        public void display(){
            System.out.println("name:");
        }
    }
    
    public static void a(InnerClass a) {
        System.out.println("内部类调用");
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.display();
    }
}

上述是一个类里面又包含了一个内部类。并且该类的方法的参数是一个内部类。

我们看下这个编译后的jvm方法签名是怎么样的, 如何去看一个class文件的方法签名呢? 我们可以通过如下的命令行:

javap -s OutClass.class

image

很明显这种情况又不行了。所以需要针对如果是内部类的情况,截取出$之后的类名。

4. 泛型的参数

public static<T> void reverses(T[] array) {
    for(int i=0;i<array.length/2;i++) {
        T temp=array[i];
        array[i]=array[array.length-i-1];
        array[array.length-i-1]=temp;
    }
}
image

这里针对解析出来的类如果是Object的情况就应该全部匹配通过。

额外的发现

前面我们讲了针对jvm的的参数需要我们自己做好映射的关系也就是从
I -> int 的转换。那实际上 asm 是提供了这样子的api进行转换处理的。
我们可以通过这样子:

import org.objectweb.asm.Type;
// 这里的desc即前面讲到的类似于 (ILjava/lang/String;)V
Type[] argumentTypes = Type.getArgumentTypes("(ILjava/lang/String;)V");
image

通过这种方式就省去了我们映射的处理逻辑了。

上一篇 下一篇

猜你喜欢

热点阅读