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

京东云开发者
• 阅读 261

一、问题描述

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导致的类型转换问题分析



此时已经可以解释为什么引入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已经被过滤掉了。

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

点赞
收藏
评论区
推荐文章
Easter79 Easter79
2年前
springboot+dubbo+vue+微信小程序实现的点单系统
后端:全部采用springbootdubbo提供接口前端:1.微信小程序    2.vueadmin管理后台,用于处理用户小程序下单信息,接单,拒单,菜单上架下架等等。特点:用户下单商家立马能够接单,websocket即时获取下单信息,并且拥有语音提示,网页页面弹出下单信息等等代码:①:后端:wec
Wesley13 Wesley13
2年前
RAC环境单实例启动数据库收到ORA
     在RAC环境中,如果你在没有启动节点的集群服务的情况下单实例启动数据库,将收到类似如下的报错:\oracle@rhel1u01\$sqlSQL\Plus:Release10.2.0.5.0ProductiononTueApr215:00:272013Copyright(
百万并发场景中倒排索引与位图计算的实践
Promise时效控单系统作为时效域的控制系统,在用户下单前、下单后等多个节点均提供服务,是用户下单黄金链路上的重要节点;控单系统主要逻辑是针对用户请求从规则库中找出符合条件的最优规则,并将该规则的时效控制结果返回客户端。
Wesley13 Wesley13
2年前
Java实现小程序微信支付
小程序支付流程交互图: !(https://oscimg.oschina.net/oscnet/39ddb4bb2025a7f5daeb0d5663bb12faba7.png)进入小程序,下单,请求下单支付,调用小程序登录API来获取Openid,生成商户订单//pages/pay/pay.jsvarappget
Stella981 Stella981
2年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Easter79 Easter79
2年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Stella981 Stella981
2年前
RabbitMQ如何保证队列里的消息99.99%被消费?
1\.本篇概要其实,还有1种场景需要考虑:当消费者接收到消息后,还没处理完业务逻辑,消费者挂掉了,那消息也算丢失了?,比如用户下单,订单中心发送了1个消息到RabbitMQ里的队列,积分中心收到这个消息,准备给这个下单的用户增加20积分,但积分还没增加成功呢,积分中心自己挂掉了,导致数据出现问题。那么如何解
Wesley13 Wesley13
2年前
Jacoco在eclipse上的集成使用
随着敏捷开发的流行,编写单元测试已经成为业界共识。但如何来衡量单元测试的质量呢?有些管理者片面追求单元测试的数量,导致底下的开发人员投机取巧,编写出大量的重复测试,数量上去了,质量却依然原地踏步。相比单纯追求单元测试的数量,分析单元测试的代码覆盖率是一种更为可行的方式。JaCoCo(JavaCodeCoverage)就是一种分析单元测试覆盖率的工具,使
消失的死锁:从 JSF 线程池满到 JVM 初始化原理剖析 | 京东云技术团队
在一次上线时,按照正常流程上线后,观察了线上报文、接口可用率十分钟以上,未出现异常情况,结果在上线一小时后突然收到jsf线程池耗尽的报警,并且该应用一共有30台机器,只有一台机器出现该问题,迅速下线该机器的jsf接口,恢复线上。然后开始排查问题。
京东云开发者 京东云开发者
3个月前
奇怪!应用的日志呢??
1.问题回顾问题背景是在进行中台应用中间件迁移过程中,发现存在项目启动失败或者项目正常启动(jsf正常挂载并正常运行,mq正常发送和消费)但是无任何日志打印现象。更奇怪的是不打印日志竟然是偶发的,在测试环境中多次部署都未出现项目启动但无日志打印情况,而且玄