当前位置: 首页 > article >正文

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

目录

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: 跳过栈帧信息。

使用方式:

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():将生成的字节码转换为字节数组。

使用方式:

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

2.1.3、 ClassVisitor

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

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

使用方式:

ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {
    @Override
    public 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():方法结束时调用。

使用方式:

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

2.1.5、 FieldVisitor

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

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

使用方式:

FieldVisitor fieldVisitor = new FieldVisitor(Opcodes.ASM9) {
    @Override
    public 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:表示设置字段的值。

使用方式:

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信息。

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 表示类的访问权限为 public
        classWriter.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表示当前对象 this
        constructor.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.kler.cn/a/503813.html

相关文章:

  • (十)ROS的常用组件——rosbag和rqt工具箱
  • RV1126+FFMPEG推流项目(3)VI模块视频编码流程
  • C++并发编程之跨应用程序与驱动程序的单生产者单消费者队列
  • hutool糊涂工具通过注解设置excel宽度
  • 《AI赋能鸿蒙Next,开启智能关卡设计新时代》
  • uniapp 之 uni-forms校验提示【提交的字段[‘xxx‘]在数据库中并不存在】解决方案
  • 【机器学习】神经网络训练技巧
  • 使用VSCode搭建Ruby on Rails集成开发环境
  • mac intel芯片下载安卓模拟器
  • 【css】浏览器强制设置元素状态(hover|focus……)
  • rclone,云存储备份和迁移的瑞士军刀,千字常文解析,附下载链接和安装操作步骤...
  • MAC AndroidStudio模拟器无网络
  • 新版懒人精灵基础老狼孩插件经典例子板块-视频教程
  • C# 内存篇
  • 《零基础Go语言算法实战》【题目 4-1】返回数组中所有元素的总和
  • 蓝牙BT04-A的使用与相关AT指令
  • AI大模型开发—1、百度的千帆大模型调用(文心一言的底层模型,ENRIE等系列)、API文档目的地
  • 多线程基础系列-多线程初识
  • kafka原理和实践
  • Linux:进程概念(二.查看进程、父进程与子进程、进程状态详解)
  • vscode的安装与使用
  • docker简单使用
  • 爬山算法与模拟退火算法的全方面比较
  • EDM 电子邮件自动化营销的关键步骤 —— 邮箱地址验证
  • C#实现条形码识别
  • 高录用快检索/JPCS独立出版-第六届新材料与清洁能源国际学术会议(ICAMCE 2025)