ASM 字节码增强

Java 之所以能够实现"一次编译,到处运行"是因为 Java 源代码经过编译器编译后生成的是固定格式的字节码(.class)文件,而不是特定于某个平台的本机机器代码。字节码是一种中间代码,它与特定平台无关。并且每个支持 Java 的平台都需要有相应的 JVM,负责解释和执行字节码。

Java 中使用命令 javac [options] <sourcefiles> 编译源码,一个 .java 源码文件从编译到运行的示例图:

Java 字节码结构

typescript 复制代码
public class ByteCodeDemo {
    
  private String prefix = "A";
​
    public String getPrefix() {
        return prefix;
    }
}

对应字节码文件 ByteCodeDemo.class

根据 JVM 规范,每个 class 文件具有固定的数据结构。

ini 复制代码
  ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             https://zhuanlan.zhihu.com/p/94498015?hmsr=toutiao.io;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
  }

可以看到 class 文件由固定结构构成:

魔数、版本号、常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表。

字节码结构详细解释,可参考官方文档:Chapter 4. The class File Format

字节码查看工具

这里介绍三种查看字节码命令的方式

方式一:

JDK 工具包的 bin 目录下提供的 javap,该工具可以查看 Java 编译后的 class 文件,使用命令如下命令进行查看。

class 文件目录下执行 javap -c 文件名.class,输出 class 字节码文件。

方式二:

Idea 插件 Bytecode Viewer。在 class 文件中点击菜单 view -> Show Bytecode

插件输出字节码文件

方式三:

Idea 插件 Jclasslib Bytecode Viewer。

class 文件中点击菜单 view -> Show Bytecode With Jclasslib

插件输出字节码信息

此插件会分好类,对于不认识的字节码指令,可以直接跳转 JDK 官网的字节码命令网页地址。

字节码增强

字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。

以下是一些常见的 Java 字节码类库:

  1. ASM (Bytecode Manipulation Framework):

    • 简介:ASM 是一个轻量级的字节码操作框架,提供了生成和转换字节码的功能。它是一个强大的字节码工具,被广泛用于许多 Java 字节码操作的场景。
    • 官方网站:ASM
  2. Byte Buddy:

    • 简介:Byte Buddy 是一个用于创建和操作字节码的库。它提供了一个高层次的 API,用于动态创建类、生成代理和拦截方法调用等。
    • 官方网站:Byte Buddy
  3. Javassist:

    • 简介:Javassist 是一个用于在运行时编辑字节码的库。它提供了简单的 API,允许开发者在不需要事先编译的情况下修改类的结构。
    • 官方网站:Javassist
  4. CGlib (Code Generation Library):

    • 简介:CGlib 是一个字节码生成库,它扩展了 Java 类。它常用于生成动态代理对象和拦截方法调用。
    • GitHub 地址:CGlib
  5. BCEL (Byte Code Engineering Library):

    • 简介:BCEL 是一个用于分析、创建和修改 Java 字节码的库。它提供了许多类和方法,用于处理类文件的各个方面。
    • 官方网站:BCEL
  6. JBC (Java Bytecode Editor):

    • 简介:JBC 是一个简单的 Java 字节码编辑器,它提供了一个图形用户界面,用于浏览、编辑和修改字节码。
    • GitHub 地址:JBC

本文则介绍 ASM 字节码增强类库

ASM

ASM是一个 Java 字节码操作和解析框架。ASM 可以在类被加载入 JVM 之前动态修改已存在类行为,也可以直接生成 .class 字节码文件。ASM 提供了和其他字节码框架相似的功能,整个类包非常小,不到120KB,但其非常注重对类字节码的操作速度。这种高性能来自于它的设计模式 - 访问者模式,即通过 Reader、Visitor 和 Writer 模式。

ASM 是直接操作类字节码数据,因此其读写对象是字节码指令。

ASM API 从组成结构上可以分成两部分,一部分为 Core API,另一部分为 Tree API

  • ASM Core API包括 asm.jarasm-util.jarasm-commons.jar
  • ASM Tree API包括 asm-tree.jarasm-analysis.jar

Core API 是基础,Tree API 也是基于 Core API 构建的。

ASM Core API

ASM Core API 使用流式的方式根据字节码结构从上到下依次处理,性能很好,所以一般 ASM 增强字节码一般都使用 Core API

核心类:

  • ClassReader:读取字节码并将其转换为内部数据结构。
  • ClassWriter:将内部数据结构转换回字节码,允许对字节码进行修改。
  • ClassVisitor:字节码访问者接口,通过它可以在访问字节码的过程中进行操作。CoreAPI 根据字节码结构从上到下依次处理,对于字节码文件中不同的区域有不同的 Visitor,比如用于访问方法的 MethodVisitor、用于访问类变量的 FieldVisitor、用于访问注解的 AnnotationVisitor 等。

基于 Core API 进行 Class Transformation 处理流程

Core API 中,使用 ClassReaderClassWriterClassVisitor 类进行 Class Transformation 的整体思路是:

rust 复制代码
ClassReader --> ClassVisitor[1] --> ...... --> ClassVisitor[N] --> ClassWriter

ClassVisitor 是 Class Transformation 的核心操作。通过 ClassVisitor 可以访问到字节码不同区域对应的 Visitor,通过对应的 Visitor 做相应的修改。

ASM Tree API

ASM Tree API 是 ASM 框架提供的一种基于树结构的字节码访问方式。将字节码文件读取到内存中构建树结构,通过各种 Node 类来映射字节码。与传统的基于事件的访问方式相比,Tree API 更直观,使开发者能够以树形结构的方式轻松分析和修改字节码。

ASM Tree API包括 asm-tree.jarasm-analysis.jar

asm-tree.jar

主要类按"包含"组织关系:

  1. ClassNode: (类)

    • 描述:表示一个类的节点。它是整个树结构的根节点。

    • 方法:

      • VisitMethod(): 用于访问类中的方法。
      • VisitField(): 用于访问类中的字段。
      • Accept(): 接受一个访问者(Visitor),允许对类进行访问。
  2. FieldNode: (字段)

    • 描述:表示一个字段的节点。它是 ClassNode 的一个子节点。

    • 方法:

      • VisitAnnotation(): 用于访问字段的注解。
  3. MethodNode: (方法)

    • 描述:表示一个方法的节点。它是 ClassNode 的一个子节点。

    • 方法:

      • VisitLocalVariable(): 用于访问方法的局部变量。
      • VisitAnnotation(): 用于访问方法的注解。
      • Instructions: 代表方法体中的指令列表。
  4. InsnList: (有序的指令集合)

    • 描述:表示一组字节码指令的列表。它通常由 MethodNode 的 Instructions 字段持有。

    • 方法:

      • Add(): 添加一个指令到列表中。
      • Accept(): 接受一个访问者,允许对指令列表进行访问。
  5. AbstractInsnNode: (单条指令)

    • 描述:表示字节码中的单个指令节点的抽象基类。
    • 子类:有各种具体的指令节点,例如 VarInsnNodeMethodInsnNode 等。

这些类和接口之间的关系形成了一个树形结构,其中 ClassNode 是根节点,MethodNodeFieldNode 是其直接的子节点,而 InsnList 包含在 MethodNode 中。通过这个树形结构,开发者可以方便地分析和修改字节码,而不需要直接操作字节码数组。

基于 ASM Tree API 进行 Class Transformation 处理流程

ASM Tree API 进行 Class Transformation 的流程,是利用 Core API 处理流程。

rust 复制代码
ClassReader --> ClassVisitor[1] --> ... --> ClassNode[M] -->... --> ClassVisitor[N] --> ClassWriter

因为 ClassNode 类(Tree API)是继承 ClassVistor 类(Core API),因此两个处理流程本质一样的。

这里需要考虑三点:

  1. 如何利用 Core APIClassReaderClassVisitor)转为 Tree APIClassNode)。
  2. 如何将 Tree APIClassNode)转为 Core APIClassVisitorClassWriter)。
  3. 如何对 ClassNode 转换。

通过下文 Demo 演示使用方式。

ASM 使用

Core API 使用 Demo

ASM 版本使用 ASM9

源码:

typescript 复制代码
public class ByteCodeDemo {
  private String prefix = "A";
​
  public String getPrefix() {
    return prefix;
  }
​
  public void test() {
    System.out.println("ByteCodeDemo#test");
  }
}

字节码操作:构建 Visitor 给每个方法开始和结尾输出标识

typescript 复制代码
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
​
public class ByteCodeDemoClassVisitor extends ClassVisitor implements Opcodes {
​
  public ByteCodeDemoClassVisitor(ClassVisitor cv) {
    super(ASM9, cv);
  }
  
  // 访问 class 文件时开始执行
  @Override
  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
    super.visit(version, access, name, signature, superName, interfaces);
  }
  
   // 访问字节码方法区时开始执行
  @Override
  public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
            String[] exceptions) {
    MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
    // 无参构造方法,这里不增强构造方法。
    if (name.equals("<init>") && mv != null) {
      return mv;
    }
    // 构造方法处理
    return new ByteCodeDemoMethodVisitor(mv);
  }
​
  private class ByteCodeDemoMethodVisitor extends MethodVisitor implements Opcodes {
​
    public ByteCodeDemoMethodVisitor(MethodVisitor mv) {
      super(ASM9, mv);
    }
    
    // 进入方法代码块开始时执行方法
    @Override
    public void visitCode() {
      super.visitCode();
            
      mv.visitFieldInsn(GETSTATIC,
                    "java/lang/System",
                    "out",
                    "Ljava/io/PrintStream;");
      mv.visitLdcInsn("start");
      mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/io/PrintStream",
                    "println",
                    "(Ljava/lang/String;)V",
                    false);
     }
    
    /**
     * 重写方法时就需要用 ASM 的写法,手动写入或者修改字节码。
     * 通过调用 MethodVisitor 的 visitXXXXInsn() 方法就可以实现字节码的插入,XXXX 对应相应的操作码助记符类型,
     * 比如 mv.visitLdcInsn("end") 对应的操作码就是ldc "end",即将字符串"end" 压入栈。
     */
    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                || opcode == Opcodes.ATHROW) {
            //方法在返回之前,打印"end"
            mv.visitFieldInsn(GETSTATIC,
                    "java/lang/System",
                    "out",
                    "Ljava/io/PrintStream;");
            mv.visitLdcInsn("end");
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/io/PrintStream",
                    "println",
                    "(Ljava/lang/String;)V",
                    false);
        }
​
        super.visitInsn(opcode);
    }
  }
}

调用方法修改字节码

java 复制代码
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
​
public class ByteCodeCodeGenerator {
​
  public static void main(String[] args) throws IOException {
​
    // 构建 ClassReader
    ClassReader classReader = new ClassReader("com/shsf/demo02/asm/demo/ByteCodeDemo");
    // ClassWriter.COMPUTE_MAXS:自动计算帧栈信息(操作数栈 & 局部变量表)
    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
​
    // 操作字节码:串联 Visitor
    ByteCodeDemoClassVisitor visitor = new ByteCodeDemoClassVisitor(classWriter);
​
    // 接受一个访问者(Visitor),允许对类进行访问
    final int parsingOptions = ClassReader.SKIP_DEBUG;
    classReader.accept(visitor, parsingOptions);
    // 获取最终字节码:生成 byte[]
    byte[] data = classWriter.toByteArray();
​
    // 输出
    File file = new File("demo02-proj06-asm/target/classes/com/shsf/demo02/asm/demo/ByteCodeDemo.class");
    System.out.println("file absolute path:" + file.getAbsolutePath());
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    fileOutputStream.write(data);
    fileOutputStream.close();
    System.out.println("success");
  }
}

结果:修改后字节码文件后,对应的反编译结果

csharp 复制代码
public class ByteCodeDemo {
  private String prefix = "A";
​
  public ByteCodeDemo() {
  }
​
  public String getPrefix() {
    System.out.println("start");
    String var10000 = this.prefix;
    System.out.println("end");
    return var10000;
  }
​
  public void test() {
    System.out.println("start");
    System.out.println("ByteCodeDemo#test");
    System.out.println("end");
  }
}

Tree API 使用 Demo

源文件

typescript 复制代码
public class ByteCodeDemo {

    private String prefix = "A";

    public String getPrefix() {
        return prefix;
    }

    public void test() {
        System.out.println("ByteCodeDemo#test");
    }

}

字节码操作类:在字节码文件中,增加类变量

java 复制代码
public class ByteCodeTreeGenerator01 {

  public static void main(String[] args) throws IOException {

    // 1、构建 ClassReader
    ClassReader classReader = new ClassReader(ByteCodeDemo.class.getName());
    // 2、构建 ClassNode
    ClassNode classNode = new ClassNode(Opcodes.ASM9);
    classReader.accept(classNode, ClassReader.SKIP_DEBUG);

    // 3、进行 transform:类中增加一个属性
    FieldNode fieldNode = new FieldNode(
            Opcodes.ACC_PUBLIC, // 表示字段的访问修饰符
            "name", // 字段的名称。
            "Ljava/lang/String;", // 字段的描述符,表示字段的类型。
            null, // 字段的签名,用于泛型类型。
            null); // 字段的初始值。
    classNode.fields.add(fieldNode);

    // 4、构建 ClassWriter
    // ClassWriter.COMPUTE_MAXS:自动计算帧栈信息(操作数栈 & 局部变量表)
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    classNode.accept(classWriter);

    // 5、生成字节码
    byte[] data = classWriter.toByteArray();

    // 输出到指定文件
    File file = new File("demo02-proj06-asm/target/classes/com/shsf/demo02/asm/demo/ByteCodeDemo.class");
    System.out.println("file absolute path:" + file.getAbsolutePath());
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    fileOutputStream.write(data);
    fileOutputStream.close();
    System.out.println("success");

  }
}

结果:修改后字节码,反编译结果

csharp 复制代码
public class ByteCodeDemo {
  private String prefix = "A";
  public String name;

  public ByteCodeDemo() {
  }

  public String getPrefix() {
    return this.prefix;
  }

  public void test() {
    System.out.println("ByteCodeDemo#test");
  }
}

ASM 工具

ASM 直接操作字节码时,需要利用一系列 VisitXXXXInsn() 方法来写对应的助记符。

面临两个问题:

1、需要知道源代码对应的各种助记符,通过 ASM 的语法转 VisitXXXXInsn()

2、ASM 写字节码时,要知道如何传参。

针对这两个问题,ASM 社区提供了工具 ASM ByteCode Outline

Idea 中直接按装此插件,可以直接把源码转为 ASM 语法格式。参考源码转换后语法,在 VisitMethod() 以及 VisitInsn() 方法中写 ASM 语法逻辑即可。

应用场景

  • 字节码增强: 实现 AOP,插入日志、性能监控等横切关注点。
  • 代码生成: 动态创建类和方法,实现动态代理。
  • 代码分析: 对现有代码进行静态分析。

参考

推荐阅读

浅谈表单受控性及结合Hooks应用

Mybatis一级缓存问题

MySQL死锁浅析

探索Taro:跨平台开发的实践与原理

spring如何使用三级缓存解决循环依赖

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

相关推荐
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠3 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries3 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_3 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平4 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码5 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞6 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb