Java Agent(三)、ASM 操作字节码入门

ops/2025/1/15 14:03:54/

目录

1、前言

2、什么是ASM?

2.1、工作流程

2.2、ASM集合核心API

2.1.1、ClassReader

2.1.2、ClassWriter

2.1.3、 ClassVisitor

2.1.4、MethodVisitor

2.1.5、 FieldVisitor

2.1.6、Opcodes

3、简单示例

3.1、maven依赖

3.2、hello world

3.3、执行结果

4、ASM和Javassist

4.1、操作层级

4.2、性能

4.3、应用场景


1、前言

在上一篇Javassist入门中,我们介绍了如何使用Javassist操作Java字节码,Javassist主要是利用Java源码以及反射机制来实现的。而今天将要介绍另一种能操作Java字节码的技术,也就是ASM。他相比Javassist更灵活,提供了更细粒度的控制。

2、什么是ASM?

ASM是一个通用的 Java 字节码操作和分析框架。它可用于修改现有类或动态生成类(直接以二进制形式)。ASM 提供了一些常见的字节码转换和分析算法,可从中构建自定义复杂转换和代码分析工具。ASM 提供与其他 Java 字节码框架类似的功能,但更注重性能。由于它的设计和实现尽可能小巧和快速,因此非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。

官网地址:ASM

2.1、工作流程

  1. 基于访问者模式:
  • ASM 使用访问者模式来遍历和修改字节码。ClassReader 类负责读取class文件并通知给 ClassVisitor 的实现。
  • ClassVisitor 接收来自 ClassReader 的事件,并可以选择性地将这些事件传递给下一个 ClassVisitor 或者修改字节码内容。
  • MethodVisitor 和 FieldVisitor 分别用于处理方法和字段相关的字节码信息。
  1. 字节码流解析:
  • ClassReader 会把 class 文件的内容解析成一系列的字节码指令流。
  • 这些指令流按照 Java 虚拟机规范定义的格式进行组织,包括常量池、访问标志、字段表、方法表等结构。
  1. 字节码生成与转换:
  • ClassWriter 类是 ASM 中用来生成新的 class 文件的核心组件。
  • 它实现了 ClassVisitor 接口,在接收到各种字节码元素后,根据需要构建出新的字节码序列。
  • 在这个过程中可以插入、删除或修改原有的字节码指令,从而实现对类行为的动态调整。

流程图如下:

2.2、ASM集合核心API

2.1.1、ClassReader

ClassReader 用于读取 .class 文件的字节码,解析其内容并提供访问类结构的方法。常用的方法有:

  • Classreader(InputStream):从输入流读取类字节码。
  • accept(ClassVisitor visitor, int flags):将类的字节码委托给ClassVisitior进行访问和处理,其中flags参数有:
    • ClassReader.EXPAND_FRAMES: 自动计算和修复栈帧。
    • ClassReader.SKIP_DEBUG: 跳过调试信息。
    • ClassReader.SKIP_FRAMES: 跳过栈帧信息。

使用方式:

java">ClassReader classReader = new ClassReader("java.lang.String");
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

2.1.2、ClassWriter

ClassWriter 用于生成或修改类的字节码,并输出为字节数组,甚至可以生成新的字节码文件。常用的方法有:

  • ClassWriter(int flags):创建 ClassWriter 对象。其中flags参数有:
    • ClassWriter.COMPUTE_MAXS:自动计算方法的最大栈深度。
    • ClassWriter.COMPUTE_FRAMES:自动计算方法的栈帧。
  • toByteArray():将生成的字节码转换为字节数组。

使用方式:

java">ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
byte[] modifiedClass = classWriter.toByteArray();

2.1.3、 ClassVisitor

ClassVisitor 是访问类结构的核心接口,所有对类的操作都需要通过它完成。常用的方法有:

  • visit: 访问类的基本信息(版本号、类名等)。
  • visitMethod:访问类中的方法。
  • visitField: 访问类中的字段。
  • visitEnd: 访问结束。

使用方式:

java">ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {System.out.println("Class name:" + name);}
};

2.1.4、MethodVisitor

MethodVisitor 用于访问和修改方法的字节码指令。常用的方法有:

  • visitCode(): 方法开始时调用。
  • visitInsn(int opcode):访问无操作数的指令。
  • visitVarInsn(int opcode, int var):访问局部变量相关指令。
  • visitLdcInsn(Object value): 插入一个常量。
  • visitEnd():方法结束时调用。

使用方式:

java">MethodVisitor methodVisitor = new MethodVisitor(Opcodes.ASM9) {@Overridepublic void visitCode() {super.visitCode();mv.visitLdcInsn("Hello, ASM!");mv.visitInsn(Opcodes.ARETURN);}
};

2.1.5、 FieldVisitor

FieldVisitor 用于访问和修改类中的字段。常用方法有:

  • visitAnnotation(String descriptor, boolean visible):访问字段上的注解。
  • visitEnd():访问字段结束。

使用方式:

java">FieldVisitor fieldVisitor = new FieldVisitor(Opcodes.ASM9) {@Overridepublic void visitEnd() {System.out.println("Field visit finished.");}
};

2.1.6、Opcodes

Opcodes 是 ASM 提供的一组常量,用于表示字节码中的操作码、访问标志等。常用的常量有:

  • 类访问标志:
    • Opcodes.ACC_PUBLIC:表示公共访问权限。
    • Opcodes.ACC_FINAL:表示不可继承。
  • 方法操作码:
    • Opcodes.RETURN:表示方法返回。
    • Opcodes.INVOKEVIRTUAL:表示调用实例方法。
  • 字段操作码:
    • Opcodes.GETFIELD:表示获取字段的值。
    • Opcodes.PUTFIELD:表示设置字段的值。

使用方式:

java">MethodVisitor mv = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "myMethod", "()V", null, null);

3、简单示例

3.1、maven依赖

<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.7.1</version>
</dependency>

3.2、hello world

使用ASM操作字节码,创建一个MyClass类,并构造一个方法,方法打印hello world, i am from asm信息。

java">package org.example.asm;import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class AsmDemo {public static void main(String[] args) throws Exception {// 创建一个 ClassWriter 实例,用于生成类的字节码ClassWriter classWriter = new ClassWriter(0);// 定义一个新的类 MyClass// Opcodes.V1_8 表示 Java 8 的版本号// Opcodes.ACC_PUBLIC 表示类的访问权限为 publicclassWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "MyClass", null, "java/lang/Object", null);// 添加默认的构造函数// Opcodes.ACC_PUBLIC 表示构造函数的访问权限为 public// <init> 表示构造函数的名称,必须为 <init>。在 Java 源代码中,编译器会自动为类生成构造方法(如果未显式声明),生成的构造方法在字节码中始终以 <init> 作为名称。<init> 是 JVM 规范中固定表示构造方法的名称。// ()V 表示构造函数的参数类型为空,即该方法无参MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);constructor.visitCode();// visitVarInsn 用于操作局部变量的字节码指令方法// Opcodes.ALOAD 表示加载一个局部变量到操作数栈上,0表示当前对象 thisconstructor.visitVarInsn(Opcodes.ALOAD, 0);// Opcodes.INVOKESPECIAL 表示调用一个特殊方法,这里调用父类 Object 的构造函数constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);constructor.visitInsn(Opcodes.RETURN); // 返回constructor.visitMaxs(1, 1); // 设置操作数栈和局部变量表的最大深度constructor.visitEnd(); // 结束构造函数的定义// 添加 sayHello 方法,同上构造函数创建MethodVisitor mv = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sayHello", "()V", null, null);mv.visitCode();// 获取 System.out// Opcodes.GETSTATIC 表示从静态字段中获取值,这里获取 System.out// Ljava/io/PrintStream; 表示 PrintStream 类型mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");// 调用 PrintStream.println 方法mv.visitLdcInsn("Hello world, i am  from ASM!"); // 将字符串 "Hello from ASM!" 压入操作数栈mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 调用 println 方法mv.visitInsn(Opcodes.RETURN); // 返回mv.visitMaxs(2, 2); // 设置操作数栈和局部变量表的最大深度mv.visitEnd(); // 结束 sayHello 方法的定义// 完成类定义classWriter.visitEnd();// 获取生成的类的字节码byte[] classData = classWriter.toByteArray();// 创建自定义类加载器MyClassLoader classLoader = new MyClassLoader();// 使用自定义类加载器加载生成的类Class<?> myClass = classLoader.defineClass("MyClass", classData);// 创建 MyClass 的实例Object instance = myClass.getDeclaredConstructor().newInstance();// 调用 MyClass 的 sayHello 方法myClass.getMethod("sayHello").invoke(instance);}// 自定义类加载器static class MyClassLoader extends ClassLoader {public Class<?> defineClass(String name, byte[] data) {// 使用父类的 defineClass 方法定义类return defineClass(name, data, 0, data.length);}}
}

3.3、执行结果

4、ASM和Javassist

上面介绍了基本的ASM用法以及API,在上一篇中讲述了Javassist的方式操作字节码。同样操作字节码的技术,我们对比一下两者的区别以及联系。

4.1、操作层级

ASM:基于字节码指令的低级别操作,直接操作字节码,接近 JVM 的底层实现。开发者需要熟悉 JVM 字节码的结构(例如操作数栈、局部变量表、指令等),更灵活但也更复杂。

Javassist:基于高层级的 API,提供类似 Java 源代码的操作方式,无需直接理解和操作字节码指令。更加高抽象,适合快速开发动态字节码功能,易于理解和使用。

4.2、性能

ASM:性能更高,因为它直接操作字节码,无额外的抽象层。更适合性能敏感的场景,例如框架底层实现或对运行时性能要求非常高的工具。

Javassist:性能略低于 ASM,因为其高层级 API 会引入一定的开销。

4.3、应用场景

ASM:用于开发高性能框架和工具,例如 AOP 框架、性能监控工具等;需要对字节码做精细控制。适合在Spring、MyBatis 等框架使用 ASM 提供底层的字节码增强能力。

Javassist:用于快速开发动态字节码功能,例如动态代理、简单方法增强;更适合业务层代码增强场景。适合快速开发,例如动态生成 POJO 类、简单的性能监控工具等。

综上,其实不难可以看出,ASM更接近于字节码底层的操作手法,天然的更具备灵活性,但是相应的代码的可读性和学习难度也较高。而Javassist更多像是个二方包,将底层字节码的操作方式封装为可读性更强的API,更方便开发者进行调用。


http://www.ppmy.cn/ops/150301.html

相关文章

nexus搭建maven私服

说到maven私服每个公司都有&#xff0c;比如我上一篇文章介绍的自定义日志starter&#xff0c;就可以上传到maven私服供大家使用&#xff0c;每次更新只需deploy一下就行&#xff0c;以下就是本人搭建私服的步骤 使用docker安装nexus #拉取镜像 docker pull sonatype/nexus3:…

.Net8 Avalonia跨平台UI框架——<vlc:VideoView>控件播放海康监控、摄像机视频(Windows / Linux)

一、UI效果 二、新建用户控件&#xff1a;VideoViewControl.axaml 需引用&#xff1a;VideoLAN.LibVLC.Windows包 Linux平台需安装&#xff1a;VLC 和 LibVLC &#xff08;sudo apt-get update、sudo apt-get install vlc libvlccore-dev libvlc-dev&#xff09; .axaml 代码 注…

【硬件介绍】Type-C接口详解

一、Type-C接口概述 Type-C接口特点&#xff1a;以其独特的扁头设计和无需区分正反两面的便捷性而广受欢迎。这种设计大大提高了用户的使用体验&#xff0c;避免了传统USB接口需要多次尝试才能正确插入的问题。Type-C接口内部结构&#xff1a;内部上下两排引脚的设计虽然可能不…

pwn-堆利用入门例题

堆入门好难啊,但十分具有挑战性 题目链接:PolarD&N 1.heap_Easy_Uaf:(Use after free) 漏洞函数在这里,向a中写入flag后free了a,但指针仍然指向a. 那么此时fast-bins中就有一个大小为0x78的堆块,a指向这个堆块,如果我们申请一个大小为0x68的堆块的话(b),a与b就都指向这个…

ubuntu各分区的用途

在 Ubuntu 中&#xff0c;分区是将硬盘划分为多个逻辑部分的过程&#xff0c;每个分区可以用于不同的用途。合理分区可以提高系统性能、数据安全性和管理效率。以下是 Ubuntu 中常见分区及其用途的详细说明&#xff1a; 1. 根分区 (/) 用途&#xff1a;存放操作系统核心文件、…

性能工具之 JMeter ActiveMQ 脚本开发实践

文章目录 一、ActiveMQ环境搭建1.1、控制台环境搭建1.2、控制台配置 二、SpringBoot开发环境搭建三、JMeter脚本二次开发环境搭建 一、ActiveMQ环境搭建 1.1、控制台环境搭建 下载 ActiveMQ 地址为&#xff1a; https://activemq.apache.org/components/classic/download/ 注…

yum和vim的使用

目录 一.Linux软件包管理器yum 二.Linux编辑器vim使用 1.vim的基本概念 2.vim的基本操作 3.vim正常模式命令 4.vim末行模式命令 5.vim的配置 一.Linux软件包管理器yum 什么是软件包 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序…

反转字符串力扣--344

目录 题目 思路 双指针法 代码 双指针法 题目 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 示例 1&#…