首页 > 其他分享 >引入JaCoCo导致的类型转换问题分析

引入JaCoCo导致的类型转换问题分析

时间:2024-08-06 11:40:36浏览次数:11  
标签:类型转换 Goods java jd TdeProxy 引入 com JaCoCo Pack

一、问题描述

JaCoCo是一款被广泛应用于公司内部的开源覆盖率工具,将其引用至测试环境后,机器启动正常,但在操作下单时出现异常,阻塞下单流程。

去除JaCoCo配置、重新编译和部署后下单功能恢复正常。堆栈信息显示,问题源于系统对请求字段进行加密时出现异常,因为无法完成类型转换抛出异常,“[Z cannot be cast to [Ljava.lang.Object”,从而阻塞下单流程。

以下为报错堆栈信息:

java.lang.ClassCastException: [Z cannot be cast to [Ljava.lang.Object;
	at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:93) 
	at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133) 
	at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:90) 
	at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133) 
	at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:90) 
	at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133) 
	at com.jd.**.TdeProxy.encryptObject(TdeProxy.java:133)
	at com.jd.**.TdeProxy.$$FastClassBySpringCGLIB$$4fa3c52.invoke(<generated>) 
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) 
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769) 
    ..省略

二、问题分析

1.报错代码

定位报错信息显示的代码位置,确认该部分代码并没有被修改过。报错提示指出属性应为数组类型,但在需要加密的类属性中并没有涉及数组类型的处理。那么“[Z”这个类型又是从何而来呢?这种情况下不禁让人怀疑,在某个时刻类可能被修改过。

报错信息中的"[Z"代表的是Java中的boolean类型数组。在Java中,基本数据类型的数组也会被表示为类似于"[Z"、"[B"、"[L"等形式的字符串,这可能是因为在程序运行过程中对类进行了动态修改或者反射操作导致的。

以下为报错处的代码片段,在将obj转换为Object[]时出现异常,既然已经识别出是数组,但是又无法完成类型转换,具体的原因需要进一步分析。

public void encryptObject(Object obj, String type) throws IllegalAccessException {
    /***省略***/
    if (Map.class.isAssignableFrom(clazz)) {
        /***省略***/
    } else if(Iterable.class.isAssignableFrom(clazz)) {
        /***省略***/
    } else if(clazz.isArray()) {
        /**********************报错代码行****************/
        for (Object o : (Object[]) obj) {
        /**********************报错代码行****************/
            this.encryptObject(o, type);
        }
    } else {
        Boolean encryptFlag = null;
        Field[] fields = this.getDeclaredFieldsAll(clazz);
        for (Field field : fields) {
            /***省略***/
        }
        /***省略***/
        for (Field field : fields) {
            Class fieldClazz = field.getType();
            if (fieldClazz == String.class) {
                /***省略***/
            } else {
                field.setAccessible(true);
                Object fieldValue = field.get(obj);
                this.encryptObject(fieldValue, type);
            }
        }
    }
}

2.分析路径

阅读代码可以看出encryptObject方法是通过递归实现的,其主要功能是对有效集合进行遍历,所以问题的重点不是递归的过程,而是推进递归过程的元素集合,集合中的元素无法正常进行类型转换导致报错,这就需要检查getDeclaredFieldsAll方法,该方法在运行时返回的集合中可能包含意料之外的元素,以下为具体实现代码:

public Field[] getDeclaredFieldsAll(Class clazz) {
    List<Field> fieldsList = new ArrayList<Field>();
    while (clazz != null) {
        Field[] declaredFields = clazz.getDeclaredFields();
        fieldsList.addAll(Arrays.asList(declaredFields));
        clazz = clazz.getSuperclass();
    }
    return fieldsList.toArray(new Field[fieldsList.size()]);
}

由于已确认引入JaCoCo后对类进行了修改,只需触发任一流程以获取类的所有属性,通过设置断点并观察集合中的元素,即可查看具体修改情况。

 

 

此时已经可以解释为什么引入JaCoCo会导致异常。报错中的类型“[Z”为合成的属性,引入JaCoCo会给类添加一个名为$jacocoData的bool数组类型属性,回到报错代码位置,出现报错是因为在识别到一个数组类型时进行了类型转换,在这里也找到了问题的答案。

涉及到合成属性/方法和JaCoCo的实现原理,下面进行简单的介绍。

3.追本溯源

(1)合成属性和方法

合成属性/方法是由Java编译器在编译过程中自动生成,并不是研发显示编写的,而是为了支持编译器内部的实现细节而生成的,下面针对合成方法进行一个举例说明。

public class Pack {
    public static void main(String[] args) {
        Pack.Goods goods = new Pack.Goods();
        System.out.println(goods.name);
    }
    private static class Goods {  
       private String name = "手机";
    }
}

将上面的代码编译一下,可以看到有三个文件,Pack$Goods.class、Pack.class、Pack$1.class,前两个一个是内部类,一个是外部类,但是最后一个类并没有被定义过,接下来分别将内部类和外部类进行反编译:

import com.jd.ryan.test.Pack.1;
class Pack$Goods {
    private String name; 
    private Pack$Goods() {
        this.name = "手机";
    }
    Pack$Goods(1 x0) {
        this(); 
    }
    static String access$100(Pack$Goods x0) {
        return x0.name; 
    }
}

内部类被反编译后,可以发现access$100的方法并没有被定义,但是分析来看name是内部类Goods的私有属性,但是外部类可以直接引用这个属性,从语法结构上讲这是被允许的,这就需要编译器在编译过程处理这种操作,在编译器看来,外部类和内部类是两个独立的类,如果外部类想要访问内部类的私有属性,其实是与封装原则相悖的。那接着看外部类的反编译结果:

public class com.jd.ryan.test.Pack {
    public com.jd.ryan.test.Pack(); 
        Code:
            0: aload_0
            1: invokespecial #1 //Method java/lang/Object."<init>":()V
    public static void main(java.lang.String[]);
        Code:
            0: new #2           //class com/jd/ryan/test/Pack$Goods
            3: dup
            4: aconst_null
            5: invokespecial #3 //Method com/jd/ryan/test/Pack$Goods."<init>":(Lcom/jd/ryan/test/Pack$1;V
            8: astore_1
            9: getstatic #4     //Field java/lang/System.out:Ljava/io/Printstream;
            12: aload_1
            13: invokestatic #5 //Method com/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods;)Ljava/lang/String;
            16: invokevirtual #6//Method java/io/PrintStream.println:(Ljava/lang/String;)V
            19: return
}

在代码实现中外部类直接打印内部类的name属性值,来看这行指令:

“Method com/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods.access$100:(Lcom/jd/ryan/test/Pack$Goods;)Ljava/lang/String;”

从字节码中表明是通过调用了内部类的access$100方法,这个方法是一个静态方法,它可以返回内部类的name属性,是Goods的私有属性,所以access$100就是编译器用来做内部访问生成的一个合成方法。

编译器可以通过生成合成属性和方法来实现一些内部优化或者内部实现,所以在使用反射机制实现一些工具时,在运行时拿到的类属性信息还可能会有一些未知的属性或者方法,这就需要工具类的代码具备一定的健壮性,对获取到的类属性进行类型转换时应该考虑到非业务字段的情况,并且能够对运行时异常进行捕获,让工具聚焦在可以处理的范围,不能影响正常的业务流程。

(2)JaCoCo原理简述

JaCoCo利用ASM在字节码中插入探针指针(Probe指针),每个探针都是一个布尔变量(true表示执行,false表示未执行)。程序运行时通过修改这些指针来检测代码的执行情况,而不会改变原始代码的行为。提到的$jacocoData数组用于保存这些执行结果,JaCoCo根据控制流类型采用不同的探针插入策略,这些探针不会改变方法的行为,只是记录它们已经执行的事实。

本文不再深入介绍JaCoCo的工作原理,感兴趣的同学可以查阅资料。

三、解决办法

通过问题分析已经确定是$jacocoData导致的,那就需要在获取属性集合的的时对这类属性进行过滤,实现方法通过isSynthetic()方法区分field属性类型,isSynthetic是Java中的一个修饰符,用于标记一个类、方法或字段是否由编译器生成。

List<Field> fieldsList = Arrays.stream(declaredFields)
                               .filter(field -> !field.isSynthetic())
                               .collect(Collectors.toList());

代码修改后,测试环境添加JaCoCo相关配置,编译部署发布后可正常下单,从断点信息来看,$jacocoData已经被过滤掉了。

 

标签:类型转换,Goods,java,jd,TdeProxy,引入,com,JaCoCo,Pack
From: https://www.cnblogs.com/Jcloud/p/18344838

相关文章

  • YOLOv9改进系列,YOLOv9引入SPDConv(新颖的卷积),用于低分辨率图像和小物体目标,实现大幅
    前言卷积神经网络在许多计算机视觉任务中取得了显著成功,例如图像分类和目标检测。然而,在图像分辨率较低或目标较小的更困难任务中,它们的性能会迅速下降。在本文中,指出这根源于现有CNN架构中一个常见但有缺陷的设计,即使用了步幅卷积和/或池化层,这导致了细粒度信息的丢失以......
  • maven 中 scope标签的作用以及引入本地jar包打包方法
    1.scope标签的作用2、使用system标签项目三方jar存放位置结构:pom.xml中引入三方jar:<dependency><groupId>test</groupId><artifactId>testa</artifactId><version>0.0.1</version><scope>system</scope><s......
  • cmake引入第三方库的debug和release版本之Windows版本
    概述#本文将介绍cmak引入第三方库debug和release不同配置。Windows上,习惯将debug模式下生成的动态库名后缀添加D以作和release区分。cmake创建一个项目A,A引入动态库B,cmake怎么配置A链接动态库B的debug和release对应的库呢本文的教程是基于 这里,如果没有看,我推荐你先看......
  • ABAP数据类型转换和不同数据类型比较
    DATA:lv_strTYPEstring,lv_str2TYPEstring,lv_charTYPEchar10,lv_iTYPEiVALUE1,lv_fTYPEpDECIMALS1VALUE'1.1'.lv_str='1.11'.lv_char='1.11'."TRUEIFlv_str=1.WRITE:1......
  • 类型转换
    1.类型转换优先级如图所示2.转换方法强制转换自动转换3.转换问题1.内存溢出:在大容量转换为小容量时,如果容量过大而超过了小容量的类所能承受的范围,则会出错。如:2.精确问题:在小数转整数时,会出现误差如:3.相乘问题:未转换,已相乘。如:解决方法:......
  • MyBatis代码生成器:SpringBoot 引入MybatisGenerator
    1.引入插件<plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.5</version>......
  • 对于泛型和类型转换的优先级
    你们猜猜谁先打印,不看答案,能猜出来吗,写在评论区下面有3道题目,分别写出答案在评论区1、classTest{publicstaticvoidMain(){Foo("Hello");}publicstaticvoidFoo(objectx){Console.WriteLine("object");......
  • Qt C++ 调用 Python 之 PyObject* 数据类型转换
    整数:PyLong_FromLong和PyLong_AsLong类型检查函数:PyLong_Check()intcppInt=42;//C++整数转换为Python整数对象PyObject*pyInt=PyLong_FromLong(cppInt);//Python整数对象转换为C++整数longcppIntFromPy=PyLong_AsLong(pyInt);Py_DECREF(pyInt)......
  • 类型转换运算符
    1.作用类通过自定义的类型转换运算符,可以将一个类型转换成另一个类型。例如将自定义的Student类转换成std::string类。虽然在格式上和运算符重载类似,但运算符重载是一个成员函数,而类型转换运算符不是,因为没有返回值。2.格式operatortype()[const];type:表示转化后的数据......
  • C++程序中的类型转换与进程异常退出血案复盘
    在C++编程中,类型转换是一个常见的操作,它允许程序员将一个数据类型转换为另一个数据类型。然而,不恰当的类型转换可能会导致未定义的行为,甚至引发进程异常退出。本文将深入分析一段C++代码,探讨其中由于类型转换不当导致的潜在问题,并解释为何这种类型转换可能引发进程异常退出......