什么是Java字节码操纵库?

  • Post category:Java

Java字节码操纵库是用来对Java字节码进行修改、分析、生成和操作的库。开发者可以使用Java字节码操纵库来创建自定义字节码,或者修复一些字节码动态生成时可能产生的问题。另外,Java字节码操纵库也是实现各种框架或者工具的基本技术之一,比如Spring AOP,Hibernate的代理实现等等。

常用的Java字节码操纵库包括ASM、Byte Buddy和Javassist等。

下面以ASM为例,讲解Java字节码操纵库的基本使用攻略:

1. 添加Maven依赖

首先需要将ASM添加到项目依赖中。我们使用Maven进行管理,只需在pom.xml文件中添加以下依赖即可:

<dependency>
  <groupId>org.ow2.asm</groupId>
  <artifactId>asm</artifactId>
  <version>9.0</version>
</dependency>

2. 创建ClassVisitor

ClassVisitor是ASM中的一个接口,需要使用它来操纵Class对象。其实现类通常是ClassReader和ClassWriter。ClassReader用于将byte数组解析成ClassVisitor可以操纵的结构,而ClassWriter用于生成符合Java字节码规范的bytecode。

这里我们以ClassWriter为例,首先需要创建一个ClassWriter对象。在ClassWriter的构造方法中,需要传入一个标识字,可以使用ClassWriter.COMPUTE_MAXS和ClassWriter.COMPUTE_FRAMES作为参数。

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);

3. 创建ClassVisitor子类

要对Class进行操作,需要创建一个ClassVisitor的子类,重写其中的方法。我们以一个添加方法的例子来解释如何创建ClassVisitor子类。

首先,我们需要实现一个MethodVisitor的子类,用于实现具体的方法逻辑。例如,在下面的示例中,我们在类中添加一行输出语句来打印“Hello, world!”:

class AddMethodVisitor extends MethodVisitor {

  public AddMethodVisitor(MethodVisitor mv) {
    super(Opcodes.ASM5, mv);
  }

  @Override
  public void visitCode() {
    visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    visitLdcInsn("Hello, world!");
    visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    super.visitCode();
  }
}

然后,我们需要实现一个ClassVisitor的子类:

class AddMethodClassVisitor extends ClassVisitor {

  public AddMethodClassVisitor(ClassVisitor cv) {
    super(Opcodes.ASM5, cv);
  }

  @Override
  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
    if ("main".equals(name)) {
      return new AddMethodVisitor(mv);
    }
    return mv;
  }
}

在visitMethod方法中,将会遍历Class中的所有方法。我们只需要将需要修改的方法的名字与当前访问的方法名匹配,就可以给该方法添加一些额外的代码。

4. 读取字节码文件并转换

通过将byte数组传入ClassReader的构造函数中可以获得ClassReader对象。之后我们需要将其转换为ClassVisitor可以操纵的形式。

byte[] classBytes = getClassBytesFromSomewhere(); // 获取要操作的字节码
ClassReader cr = new ClassReader(classBytes);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
AddMethodClassVisitor cv = new AddMethodClassVisitor(cw);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
byte[] modifiedClassBytes = cw.toByteArray(); // 获取修改后的字节码

在执行cr.accept方法时,我们需要传入一个ClassVisitor对象。该对象会根据访问到的内容构建出一个类树,生成修改后的字节码。

至此,使用ASM进行字节码操纵的流程就结束了。下面给出另一个简单的示例将readBoolean和readByte这两个方法的实现代码替换成简化版的实现:

class Replacer extends MethodVisitor {
  private static final String desc = "(Ljava/io/DataInput;)Z";
  private static final String newMethod = "(Ljava/io/DataInput;)Z";
  private static final String newMethodDesc = "(Ljava/io/DataInput;)Z";
  private static final String READ_BOOLEAN_METHOD = "readBoolean";
  private static final String READ_BYTE_METHOD = "readByte";

  public Replacer(MethodVisitor mv) {
    super(Opcodes.ASM4, mv);
  }

  @Override
  public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
    if (opcode == INVOKEINTERFACE && name.equals(READ_BOOLEAN_METHOD) && desc.equals(this.desc)) {
      super.visitVarInsn(ALOAD, 1);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/DataInput", "readUnsignedByte", "()I", false);
      mv.visitInsn(ICONST_1);
      mv.visitInsn(ISUB);
      Label l1 = new Label();
      mv.visitJumpInsn(IFNE, l1);
      mv.visitInsn(ICONST_0);
      Label l2 = new Label();
      mv.visitJumpInsn(GOTO, l2);
      mv.visitLabel(l1);
      mv.visitInsn(ICONST_1);
      mv.visitLabel(l2);
      mv.visitInsn(IRETURN);
    } else if (opcode == INVOKEINTERFACE && name.equals(READ_BYTE_METHOD) && desc.equals(this.desc)) {
      super.visitVarInsn(ALOAD, 1);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/DataInput", "readByte", "()B", false);
      mv.visitInsn(I2L);
      mv.visitInsn(LRETURN);
    } else {
      super.visitMethodInsn(opcode, owner, name, desc, itf);
    }
  }
}

class ReplaceManipulator extends ClassVisitor {
  private static final String DESCRIPTOR_TO_REPLACE = "(Ljava/io/DataInput;)Z";

  public ReplaceManipulator(ClassVisitor cv) {
    super(Opcodes.ASM5, cv);
  }

  @Override
  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
    if (!desc.equals(DESCRIPTOR_TO_REPLACE)) {
      return mv;
    }
    return new Replacer(mv);
  }
}

byte[] classFileBytes = readFileFromSomewhere();
ClassReader cr = new ClassReader(classFileBytes);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ReplaceManipulator rm = new ReplaceManipulator(cw);
cr.accept(rm, 0);
byte[] patchedClassBytes = cw.toByteArray();

这里我们首先分别将四个常量赋值给四个类级别的字段,随后实现扩展MethodVisitor的Replacer子类。在其中的visitMethodInsn方法中,我们判断当前方法是否为读取Boolean值或读取Byte值方法。如果是,则分别调用readUnsignedByte()方法或readByte()方法;然后使用ICONST_1、ICONST_0等指令判断返回类型并返回对应值。在visitMethod中的返回值中,我们使用新的Replacer对象代替原始MethodVisitor。

最后,我们创建一个ReplaceManipulator对象并实现了visitMethod方法,以控制遍历Java类中的所有方法。在visitMethod方法中,我们只为具有特定引用的方法创建一个新的Replacer必要方法,并使用它进行处理。这里我们使用了COMPUTE_MAXS标志来通知ASM,我们需要在实例化方法visitor时计算本地变量和堆栈大小。

以上就是Java字节码操纵库的使用攻略。