使用ASM框架创建ClassVisitor时遇到IllegalArgumentException的一种可能解决办法

news2024/12/23 5:59:27

背景

ASM是java语言中最为广泛使用的插装框架,其优点在于可以动态地在运行时改变java系统的行为,加入我们自己的逻辑。在软件测试领域应用广泛。但是其使用难度很高,一方面使用asm框架需要对java底层知识有较高的了解,另一方面网上关于asm的资料较少出现问题经常难以搜索到解决方案。参考资料[1]-[3]提供了一些关于asm的基础介绍。

使用ASM时一个非常大的问题在于我们往往需要将自己的少量逻辑插入到复杂的目标系统中进行测试,而我们对目标系统却没有很深的理解。这样当由于目标系统一些特性使得我们的插装代码出错时,我们就很容易处于一种无处下手的状态。

我在使用ASM插装一个不算太复杂的框架raft-java时,创建ClassVisitor总是会在构造函数出爆出IllegalArgumentException,这个问题困扰了我两天,最后发现是由于系统中多次引用asm后声明版本号不一致导致。在此记录下来以供其他的asm初学者进行参考。

问题描述

使用ASM插装raft-java时,遇到问题如下:
raft-java插装点:ServerMain类的main方法的server.start()语句前。
在这里插入图片描述
在server.start()执行前插入我们自己的函数逻辑。
插装逻辑如下图。

ClassReader cr = new ClassReader(input);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
RaftDownCLassVIsitor rcv = new RaftDownCLassVIsitor(ASM9, cw);
cr.accept(rcv, 0);
byte[] contents = cw.toByteArray();

其中RaftDownClassVisitor是我自己写的插装类,这里只摘录其构造函数和visitMethod函数。

public RaftDownCLassVIsitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        logger.info("visitMethod" + name);
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (className.equals("com/github/wenweihu86/raft/example/server/ServerMain")) {
            logger.info("visit com/github/wenweihu86/raft/example/server/ServerMain");
            RaftDownMethodVisitor rmv = new RaftDownMethodVisitor(ASM9, mv);
            rmv.setIndex(10);
            return rmv;
        }
        return mv;
    }

其中RaftDownMethodVisitor类是我们自己写的MethodVisitor插装类。上面的visitMethod方法意思为当程序执行到ServerMain类的函数时,其返回的函数会是我们的RaftDownMethodVisitor提供的修改过的函数。
RaftDownMethodVisitor中则进一步判断,当调用server.start()语句时,先执行一些我们的逻辑。RaftDownMethodVisitor和本篇博客所阐释的问题关系不大,这里不再展开。
使用上述代码进行实验时,系统报错:
在这里插入图片描述

解决过程

首先根据堆栈信息,查看ClassVisitor的构造函数。

public ClassVisitor(final int api, final ClassVisitor classVisitor) {
    if (api != Opcodes.ASM9
        && api != Opcodes.ASM8
        && api != Opcodes.ASM7
        && api != Opcodes.ASM6
        && api != Opcodes.ASM5
        && api != Opcodes.ASM4
        && api != Opcodes.ASM10_EXPERIMENTAL) {
      throw new IllegalArgumentException("Unsupported api " + api);
    }
    if (api == Opcodes.ASM10_EXPERIMENTAL) {
      Constants.checkAsmExperimental(this);
    }
    this.api = api;
    this.cv = classVisitor;
  }

可以看到构造函数确实可以抛出IllegalArgumentException。但是从源码来看我们的输入并不应该触发这个异常才对。除此之外,从源码来看,抛出的IllegalArgumentException应该有“Unsupported api”的信息才对,但是我却打印不出来该信息。

于是上网上寻找资料(参考资料[4]-[7])。在过程中,发现一些人在使用asm插装springboot, gwt等项目时也产生过类似问题。解决方案则大多时将目标系统或者asm调整至固定版本。这引发人怀疑,会不会我们也是asm的版本出了问题呢?

于是我使用mvn dependency:tree命令查看了raft-java的依赖。
在这里插入图片描述
果然找到了raft-java自身依赖的asm,而且还是间接依赖,这样的依赖单纯看源码几乎不可能看出。raft-java本身使用了5.2版本的asm,而我们的插装代码则使用了9.1版本的asm并在创建RaftDownClassVisitor和RaftDownMethodVisitor时将asm版本指定成了9.(RaftDownCLassVIsitor rcv = new RaftDownCLassVIsitor(ASM9, cw);)。将这里传入的参数改为ASM5即可消除错误。

解决方法

使用mvn dependency:tree命令检查目标系统有没有使用asm依赖。若有,则在创建ClassVisitor和MethodVisitor实例时需要注意传入的ASM版本号参数要和目标系统依赖的asm版本一致。

RaftDownCLassVIsitor rcv = new RaftDownCLassVIsitor(ASM5, cw);
RaftDownMethodVisitor rmv = new RaftDownMethodVisitor(ASM5, mv);

一些经验教训

使用try…catch…捕捉异常

一些目标系统会使用全局性的try…catch…机制,导致即使代码在某一步出错,但是系统不会在这里报错。继续运行的系统可能会产生错误的行为,但是让人难以和出错的代码联系起来。
在本次debug过程中,ClassVisitor的构造函数的异常只有在我将插装代码改为如下形式后才会被打印出来。

ClassReader cr = new ClassReader(input);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
try {
        RaftDownCLassVIsitor rcv = new RaftDownCLassVIsitor(ASM9, cw);
        // RaftDownCLassVIsitor rcv = new RaftDownCLassVIsitor(ASM5, cw);
        cr.accept(rcv, 0);
    } catch (Exception e) {
        logger.info(e.getMessage());
        e.printStackTrace();     
    }
byte[] contents = cw.toByteArray();

因此,当我们怀疑某一步出了问题时,应注意主动加上try…catch…使得异常可以被我们自己捕获。

注意vscode和mvn dependency:tree展示的依赖的差别

使用vscode的java插件,我们也可以看到一个项目的依赖
在这里插入图片描述
但是可以发现,vscode中展示出来的目标系统依赖的asm的版本(6.0_alphe)和mvn dependency:tree展示出来的asm的版本(5.2)并不一致。
在本文所展示的例子中,只有使用mvn dependency:tree展示出来的asm-5.2版本才能阻止错误发生。

讨论

在本文所展示的例子中,目标系统raft-java是maven项目,而我所写的插装代码则是gradle项目。我不清楚目标项目和插装代码的构建工具不同是否会导致依赖混淆。换言之,我不确定假如我使用maven作为插装代码的构建工具文中所提到的bug还会不会产生。此问题留待以后研究。

参考资料

[1] https://asm.ow2.io/
[2] https://asm.ow2.io/asm4-guide.pdf
[3] raft-java仓库地址
[4] https://stackoverflow.com/questions/67590531/gwt-application-fails-with-illegalargumentexception-in-org-objectweb-asm-classvi
[5] https://cloud.tencent.com/developer/article/1918986
[6] https://stackoverflow.com/questions/25403911/illegalargumentexception-at-org-springframework-asm-classreader-when-initializin
[7] https://issues.apache.org/jira/browse/BEANUTILS-477

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/165384.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网页共享电脑屏幕与播放(带声音)

这次项目我们是写的一个课堂辅助软件的网页版,其中有一个功能感觉能作为我们项目的一个亮点,就是直播功能,在之前并没有写过这个东西。虽然现在这个功能还不知道怎么写,但是它的流程终归是利用视频流将本地的视频给共享出去&#…

Verilog:【8】基于FPGA实现SD NAND FLASH的SPI协议读写

碎碎念: 终于熬过了期末周,可以开始快乐的开发之旅了。 这一期作为一千粉后的首篇博客,由于之后项目会涉及到相关的部分,因此介绍的是使用FPGA实现SD NAND FLASH的读写操作,以雷龙科技提供的SD NAND FLASH样品为例&…

实证分析权重系数计算大全

在实际研究中,权重计算是一种常见的分析方法,需要结合数据的特征情况进行选择,比如数据之间的波动性是一种信息量,那么可考虑使用CRITIC权重法或信息量权重法;也或者专家打分数据,那么可使用AHP层次法或优序…

直观感受PromQL及其数据类型

由于PromQL内容较多,将内容分为三篇文章讲述: 一、直观感受PromQL及其数据类型 二、PromQL之选择器和运算符 三、PromQL之函数 想必都知道要使用Msql,必须会用SQL,同样要使用Prometheus 就要掌握PromQL(Prometheus Que…

【链表】leetcode142.环形链表II(C/C++/Java/Js)

leetcode142.环形链表II1 题目2 思路2.1 判断链表是否有环--快慢指针法2.2 如果有环,如何找到这个环的入口2.3 补充3 代码3.1 C版本3.2 C版本3.3 Java版本3.4 JavaScript版本4 总结1 题目 题源链接 给定一个链表的头节点 head ,返回链表开始入环的第一个…

软测复习05:基于质量特征的测试

作者:非妃是公主 专栏:《软件测试》 个性签:顺境不惰,逆境不馁,以心制境,万事可成。——曾国藩 文章目录性能测试压力测试容量测试健壮性测试安全性测试可靠性测试恢复性测试协议一致性测试兼容性测试安装…

【数据结构】保姆级单链表教程(概念、分类与实现)

目录 🍊前言🍊: 🍈一、链表概述🍈: 1.链表的概念及结构: 2.链表存在的意义: 🍓二、链表的分类🍓: 🥝三、单链表的实现&#x1f…

​盘点几款国内外安全稳定的域名解析平台​

众所周知,有了域名后想建站使用,必须要先解析域名。域名使用注册商一般会提供域名解析服务,这虽然为用户提供了方便,但功能大多有限,使用第三方域名解析平台就成了非常必要的选择。今天,小编就为大家盘点几…

计算机视觉OpenCv学习系列:第四部分、键盘+鼠标响应操作

第四部分、键盘鼠标响应操作第一节、键盘响应操作1.键盘响应事件2.键盘响应3.代码练习与测试第二节、鼠标操作与响应1.鼠标事件与回调2.鼠标操作3.代码练习与测试学习参考第一节、键盘响应操作 键盘响应中有一个函数叫做waitKey,所有的获取键盘键值都是通过waitKey…

【经典笔试题】动态内存管理

test1:void GetMemory(char* p) {p (char*)malloc(100); } void Test(void) {char* str NULL;GetMemory(str);strcpy(str, "hello world");printf(str); }int main() {Test();return 0; }请问执行上面代码,会出现什么结果?解析&a…

7. R语言【独立性检验】:卡方独立性检验、Fisher精确检验 、Cochran-Mantel-Haenszel检验

文章目录1. 卡方检验2. 费希尔精确检验(Fisher Exact Test)3. Cochran-Mantel-Haenszel检验独立性检验:用来判断变量之间相关性的方法,如果两个变量彼此独立,那么两者统计上就是不相关的 1. 卡方检验 可以使用chisq.…

Java面向对象之多态、内部类、常用API

目录面向对象之三大特性之三:多态多态的概述、多态的形式多态的好处多态下引用数据类型的类型转换多态的综合案例内部类内部类概述内部类之一:静态内部类内部类之二:成员内部类内部类之三:局部内部类内部类之四:匿名内…

JavaSE与网络面试题

大佬的: https://github.com/Snailclimb/JavaGuide https://osjobs.net/topk/all/ 自增自减 要点: 赋值 ,最后计算 右边的从左到右加载值,一次压入操作数栈 实际先算哪个看运算符的优先级 自增、自减操作都是直接修改变量…

SpringCloud面试题

为什么需要学习Spring Cloud 不论是商业应用还是用户应用,在业务初期都很简单,我们通常会把它实现为单体结构的应用。但是,随着业务逐渐发展,产品思想会变得越来越复杂,单体结构的应用也会越来越复杂。这就会给应用带…

带你走入虚函数和多态的世界(c++)

1、什么是虚函数 C类中用virtual修饰的函数叫做虚函数&#xff0c;构造函数没有虚构造函数&#xff0c;存在虚析构函数&#xff0c;C所有虚函数都是一个指针去存储的&#xff0c;所以具有虚函数的类&#xff0c;内存会增加一个指针大小的内存 #include<iostream> #includ…

第一章:计算机网络概述

一、计算机网络基本概念 1、什么是计算机网路&#xff1f; 计算机网络是通信技术与计算机技术紧密结合的产物。计算机网络就是一种特殊的通信网络&#xff0c;其特别之处就是&#xff0c;其信源和信宿通常就是我们所说的计算机&#xff0c;发出的信息通常就是数字化的一些信息…

数据分析-深度学习 Pytorch Day5

李宏毅《机器学习》第6讲——梯度下降Review: 梯度下降法在回归问题的第三步中&#xff0c;需要解决下面的最优化问题&#xff1a;我们要找一组参数θ &#xff0c;让损失函数越小越好&#xff0c;这个问题可以用梯度下降法解决。假设θ有里面有两个参数θ1,θ2&#xff0c;随机…

FPGA 20个例程篇:19.OV7725摄像头实时采集送HDMI显示(一)

第七章 实战项目提升&#xff0c;完善简历 19.OV7725摄像头实时采集送HDMI显示&#xff08;一&#xff09; 在例程“OV7725摄像头实时采集送HDMI显示”中&#xff0c;我们将走近FPGA图像处理的世界&#xff0c;图像处理、数字信号、高速接口也一直被业界公认为FPGA应用的三大主…

k8s ingress概念和实践

什么是Ingress Ingress 是对集群中服务的外部访问进行管理的 API 对象&#xff0c;典型的访问方式是 HTTP/HTTPS 该特性从1.19版本开始作为stable状态进行发布 Ingress 公开从集群外部到集群内服务的 HTTP 和 HTTPS 路由。 流量路由由 Ingress 资源上定义的规则控制。 如下…

Python算法:三种简单排序的方法

目录 前言 1、插入排序 实例 2、选择排序 实例 3、冒泡排序 实例 前言 声明&#xff1a;本文所有动图来源为菜鸟教程 &#x1f340;作者简介&#xff1a;被吉师散养、喜欢前端、学过后端、练过CTF、玩过DOS、不喜欢java的不知名学生。 &#x1f341;个人主页&#xff1a;红…