字节码操作的手术刀-Javassist

Javassist

前面文章介绍的 ASM 入门门槛还是挺高的,需要跟底层的字节码指令打交道,优点是小巧、性能好。Javassist 是一个性能比 ASM 稍差但是使用起来简单很多的字节码操作库,不需要了解字节码指令,由东京工业大学的数学和计算机科学系的教授 Shigeru Chiba 开发.

Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。

Javassist作用

  • 动态代理 Javassist可以在运行时生成代理类,从而实现AOP编程,比如在方法调用前后增加日志、权限控制等功能。

  • 动态生成类 Javassist可以在运行时动态地生成新的类,这个特性在一些框架中被广泛使用。

  • 类文件编辑 Javassist可以在运行时修改类的字节码,从而实现一些功能,比如动态修改类的字段、方法等。

  • 字节码分析 Javassist可以对字节码进行分析,提取类的结构信息,比如类名、字段、方法等。

核心 API

alt 在 Javassist 中每个需要编辑的 class 都对应一个 CtClass,CtClass 的含义是编译时的类("compile time class"),这些类会存储在 ClassPool 中,ClassPool 是一个容器,存储了一系列 CtClass 对象。

Javassist 的 API 与 Java 反射 API 比较相似,Java 类包含的字段、方法在 Javassist 中分别对应 CtField 和 CtMethod,通过 CtClass 对象就可以给类新增字段、修改方法了。

alt

CtMethod

对应一个 class 文件,CtMethod 对应一个方法,ClassPool 是 CtClass 的集合。

CtClass 的 writeFile 可以将生成的 class 文件输出到指定目录中。新建一个 Hello 类可以用下面的代码:

ClassPool cp = ClassPool.getDefault();
CtClass ct = cp.makeClass("ya.me.Hello");
ct.writeFile("./out");

运行上面的代码,out 目录下就生成了一个 Hello 类,内容如下:

package ya.me;

public class Hello {
    public Hello() {
    }
}

给已有类新增方法 有一个空的 MyMain 类,如下所示:

public class MyMain {

}

接下来用 Javassist 给它新增一个 foo 方法。

ClassPool cp = ClassPool.getDefault();
cp.insertClassPath("/path/to/MyMain.class");
CtClass ct = cp.get("MyMain");
CtMethod method = new CtMethod(
        CtClass.voidType,
        "foo",
        new CtClass[]{CtClass.intType, CtClass.intType},
        ct);
method.setModifiers(Modifier.PUBLIC);
ct.addMethod(method);
ct.writeFile("./out");

查看生成 MyMain 类可以看到生成了 foo 方法

public void foo(int var1, int var2) {
}

修改方法体

CtMethod 提供了几个实用的方法来修改方法体:

  • setBody 方法来替换整个方法体,setBody 方法接收一段源代码字符串,Javassist 会将这段源码字符串编译为字节码,替换原有的方法体。
  • insertBefore、insertAfter:在方法开始和结束插入语句。
public int foo(int a, int b) {
    return a + b;
}

比如想把 foo 方法体修改为 "return 0;",可以这么修改:

...
CtMethod method = ct.getMethod("foo""(II)I");
method.setBody("return 0;");
...

生成的修改过的 foo 方法如下:

public int foo(int var1, int var2) {
    return 0;
}   

如果想把 foo 方法体的 a + b; 修改为 "a * b",是不是可以用下面的代码:

method.setBody("return a * b;")

运行报错,提示找不到字段 a:

Exception in thread "main" javassist.CannotCompileException: [source error] no such field: a
 at javassist.CtBehavior.setBody(CtBehavior.java:474)
 at javassist.CtBehavior.setBody(CtBehavior.java:440)
 at JavassistTest2.main(JavassistTest2.java:17)
Caused by: compile error: no such field: a

这是因为源代码在 javac 编译以后,局部变量名字都被抹去了,只留下了类型和局部变量表的位置,比如上面的 a 和 b 对应局部变量表 1 和 2 的位置,位置 0 由 this 占用。

在 Javassist 中访问方法参数使用 $0 $1 ...,而不是直接使用变量名,把上面的代码改为:

method.setBody("return $1 * $2;");

生成新的 MyMain 类中 foo 方法已经变

public int foo(int var1, int var2) {
    return var1 * var2;
}

除了方法的参数,Javassist 定义了以 $ 开头的特殊标识符,如下表所示:

alt

下面来逐一介绍

  • $0 $1 $2 ... 方法参数
  • 0 等价于 this,如果是静态方法 $0 不可用,从 $1 开始依次表示方法参数
  • $args 参数
  • $args变量表示所有参数的数组,它是一个 Object 类型的数组,如果参数中有原始类型,会被转为对应的对象类型,比如上面 foo(int a, int b) 对应的 $args
new Object[]{ new Integer(a), new Integer(b) }
  • ?参数 ? 参数表示所有的参数的展开,参数直接用逗号分隔,
foo(?)

相当于:

foo($1$2, ...)

  • $cflow $cflow 是 "control flow" 的缩写,这是一个只读的属性,表示某方法递归调用的深度。一个典型的使用场景是监控某递归方法执行的时间,只想记录一次最顶层调用的时间,可以使用 $cflow 来判断当前递归调用的深度,如果不是最顶层调用忽略记录时间。

比如下面的计算 fibonacci 的方法:

public long fibonacci(int n) {
    if (n <= 1) return n;
    else return fibonacci(n-1) + fibonacci(n-2);
}

如果只想在第一次调用的时候执行打印,可以用下面的的代码:

CtMethod method = ct.getMethod("fibonacci""(I)J");
method.useCflow("fibonacci");
method.insertBefore(
        "if ($cflow(fibonacci) == 0) {" +
            "System.out.println(\"fibonacci init \" + $1);" +
        "}"
);

执行生成的 MyMain,可以看到只输出了一次打印:

java -cp /path/to/javassist.jar:. MyMain
fibonacci init 10

  • $_ 参数 CtMethod 的 insertAfter() 方法在目标方法的末尾插入一段代码。 $_ 来表示方法的返回值,在 insertAfter 方法可以引用。比如下面的代码:
method.insertAfter("System.out.println(\"result: \"  + $_);");

查看反编译生成的 class 文件:

public int foo(int a, int b) {
    int var4 = a + b;
    System.out.println(var4);
    return var4;
}

细心的读者看到这里会有疑问,如果是方法异常退出,它的方法返回值是什么呢?假如 foo 代码如下:

public int foo(int a, int b) {
    int c = 1 / 0;
    return a + b;
}

执行上的改写后,反编译以后代码如下:

public int foo(int a, int b) {
    int c = 1 / 0;
    int var5 = a + b;
    System.out.println(var5);
    return var5;
}

这种情况下,代码块抛出异常,是无法执行插入的语句的。如果想代码抛出异常的时候也能执行,就需要把 insertAfter 的第二个参数 asFinally 设置为 true:

method.insertAfter("System.out.println(\"result: \"  + $_);"true);

执行输出结果如下,可以看到已经输出了 "result: 0"

result: 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
        at MyMain.foo(MyMain.java:9)
        at MyMain.main(MyMain.java:6)

还有几个 Javassist 提供的内置变量($r等),用的非常少,这里不再介绍,具体可以查看 javassist 的官网。

小结

本文的内容主要介绍了 Javassist 这个非常广泛的字节码改写工具,详细介绍了它们的 API 和常见使用场景,后续我们将看到更多这两个工具的应用。

本文由 mdnice 多平台发布

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

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

相关文章

Unittest 笔记:unittest拓展生成HTM报告

HTMLTestRunner 是一个unitest拓展可以生成HTML 报告 下载地址&#xff1a;GitHub: https://github.com/defnnig/HTMLTestRunner HTMLTestRunner是一个独立的py文件&#xff0c;可以放在Lib 作为第三方模块使用或者作为项目的一部分。 方式1&#xff1a; 验证是否安装成功&…

TSC TTP-244条码打印机如何批量打印二维码

二维码的应用可以说是非常的普遍了&#xff0c;二维码在应用之前不但需要条码打印机批量打印二维码&#xff0c;还需要相关的二维码制作软件制作二维码。今天小编就教大家用TSC TTP-244条码打印机批量打印二维码。 1、打开二维码制作软件&#xff0c;新建一个标签&#xff0c;选…

条码打印机如何打印流水号

流水号现在用途也是非常广泛的&#xff0c;应用于各行各业&#xff0c;今天小编就教大家如何用条码打印机打印流水号&#xff0c;操作也是非常简单&#xff0c;先用条码打印软件生成流水号&#xff0c;然后连接条码打印机打印流水号。 打开条码打印软件&#xff0c;新建标签&a…

条码打印软件如何连接激光打印机打印条码标签

在连接打印机打印条码标签之前&#xff0c;需要对条码打印软件有一个简单的了解&#xff0c;条码打印软件是通过驱动来连接各种打印机进行打印条码标签的&#xff0c;所以在连接激光打印机打印条码标签时&#xff0c;需要在电脑上安装通用激光打印机驱动。接下来我们看看过程。…

反转链表+交换两个链表的节点

目录 ​编辑 一&#xff0c;反转链表 1.题目描述 2.例子 3.题目接口 4.分析以及解题代码 1.迭代法 2.递归写法 二&#xff0c;两两交换两个链表中的节点 1.题目描述 2.例子 3.题目接口 4.题目分析以及解法 一&#xff0c;反转链表 1.题目描述 首先来看看反转链表的…

86. 分隔链表(中等系列)

给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。 示例 1&#xff1a; 输入&#xff1a;head [1,4,3,2,5,2], x 3 输出&…

hiredis的安装与使用

hiredis的介绍 Hiredis 是一个用于 C 语言的轻量级、高性能的 Redis 客户端库。它提供了一组简单易用的 API&#xff0c;用于与 Redis 数据库进行交互。Hiredis 支持 Redis 的所有主要功能&#xff0c;包括字符串、哈希、列表、集合、有序集合等数据结构的读写操作&#xff0c…

Docker vs. Podman: 选择容器技术的智慧之选

嗨&#xff0c;各位亲爱的程序员小伙伴们&#xff01;当我们步入容器技术的世界&#xff0c;往往会在众多选择中迷茫。两个备受瞩目的容器工具&#xff0c;Docker 和 Podman&#xff0c;都在业界掀起了一股风潮。今天&#xff0c;我将带你深入探索&#xff0c;为什么在 Docker …

购买的gmail谷歌邮箱,faceboolhotmail邮箱mail邮箱yahoo,aol在国外使用完全不受影响,购买地址推荐:

购买的谷歌邮箱,faceboolhotmail邮箱mail邮箱yahoo,aol在国外使用完全不受影响&#xff0c;购买地址推荐&#xff1a;邮箱谷歌批发购买地址&#xff1a;buyemail.buyaccountemail.com记好了 登录方法如下 1、下载QQ邮箱手机客户端 2、先使用QQ邮箱登陆到客户端 谷歌邮箱 …

免费激活Yahoo邮箱的POP3服务

通过POP3&#xff0c;我们就能够在本机上使用各种邮件客户端软件(Foxmail、Outlook等)收发电子邮件。 Yahoo免费邮箱没有提供免费POP3服务&#xff0c;而通过邮箱里的设置激活该服务时则被提示需要收费。如图1所示 图1 笔者就给大伙介绍一个小技巧&#xff0c;可以免费地打开Ya…

Foxmail6下@yahoo.cn邮箱设置

http://www.88sina.com/foxmail-yahoo.cn/(转) 昨天申请了一个yahoo.cn的邮箱&#xff0c;在Foxmail中弄了半天&#xff0c;就是使用不了&#xff0c;不是提示输密码就是提示这样那样的错误&#xff0c;今天在网上找来找去&#xff0c;试来试去&#xff0c;终于可以正常收发邮件…

雅虎邮箱 找回密码_如何恢复被遗忘的Yahoo! 密码

雅虎邮箱 找回密码 If you don’t use a password manager, those complex passwords can be pretty hard to remember. If you’ve forgotten your Yahoo password, you can’t really recover that same password, but it’s easy enough to recover your account by resetti…

类似于yahoo邮箱登陆的提示效果

当鼠标聚焦到邮箱地址文本框时&#xff0c;文本框内的“请输入邮箱地址”文字被清空。 效果图&#xff1a; <% Page Language"C#" AutoEventWireup"true" CodeFile"类似于yahoo邮箱登陆的提示效果.aspx.cs" Inherits"类似于yahoo邮箱登…

java雅虎邮件发送

java雅虎邮件发送 1、在网页上登录雅虎邮箱-需翻墙2、登录成功后台&#xff0c;进入账号资料3、进入账户安全&#xff0c;开启双重验证4、创建应用5、替换配置中的邮箱密码即可使用 申请雅虎邮箱后&#xff1a; application.yml配置 spring:mail:host: smtp.mail.yahoo.compo…

【kubernetes】使用kubepshere部署中间件服务

KubeSphere部署中间件服务 入门使用KubeSphere部署单机版MySQL、Redis、RabbitMQ 记录一下搭建过程 (内容学习于尚硅谷云原生课程) 环境准备 VMware虚拟机k8s集群&#xff0c;一主两从&#xff0c;master也作为工作节点&#xff1b;KubeSphere k8skubesphere devops比较占用磁…

Visual Studio 2017安装和项目配置

目录 前言1. What、Why and How1.1 What1.2 Why1.3 How 2. 安装3. 创建新项目4. 配置OpenCV库4.1 下载opencv安装包4.2 配置系统环境变量4.3 VS项目环境配置4.4 总结 5. 已有项目添加6. Tips6.1 常用快捷键6.2 字体和颜色选择6.3 配置编译路径 结语下载链接参考 前言 最近因为项…

scratch3.0接苹果小游戏

Scratch是可视化的编程语言&#xff0c;其丰富的学习环境适合所有年龄阶段的人。利用它可以制作交互式程序、富媒体项目&#xff0c;包括动画故事、读书报告、科学实验、游戏和模拟程序等。与其他编程语言相比&#xff0c;Scratch的可视化编程环境让我们更容易领略编程的魅力 今…

接水果游戏代码 c语言,制作接水果游戏

今天是儿童节&#xff0c;让老师教同学们做个接水果的小游戏吧。 我们新建一个项目&#xff0c;把小猫角色删除&#xff0c;然后选择一个碗的角色来接水果: 把碗移动到白色画布的下半部分&#xff0c;让它可以随着鼠标的移动而左右移动&#xff0c;但是不需要上下移动。方法就是…