1. 概述

本文将介绍 Javassist(Java Programming Assistant) 库。简单来说,这个库通过提供比 JDK 更高级的 API,让 Java 字节码操作变得轻而易举。✅

2. Maven 依赖

在项目中添加 Javassist 库非常简单,只需在 pom.xml 中引入以下依赖:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>${javaassist.version}</version>
</dependency>

<properties>
    <javaassist.version>3.21.0-GA</javaassist.version>
</properties>

3. 什么是字节码?

从高层次看,所有用纯文本格式编写的 Java 类都会被编译成字节码——一组能被 Java 虚拟机(JVM)处理的指令集。JVM 会将这些字节码指令翻译成机器级的汇编指令。⚠️

假设我们有一个 Point 类:

public class Point {
    private int x;
    private int y;

    public void move(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // 标准构造器/getter/setter
}

编译后会生成包含字节码的 Point.class 文件。我们可以通过 javap 命令查看字节码:

javap -c Point.class

输出如下:

public class com.baeldung.javasisst.Point {
  public com.baeldung.javasisst.Point(int, int);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iload_1
       6: putfield      #2                  // Field x:I
       9: aload_0
      10: iload_2
      11: putfield      #3                  // Field y:I
      14: return

  public void move(int, int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field x:I
       5: aload_0
       6: iload_2
       7: putfield      #3                  // Field y:I
      10: return
}

这些指令由 Java 语言规范定义(指令列表很丰富)。我们分析下 move() 方法的字节码:

  • aload_0:从局部变量 0 加载引用到栈
  • iload_1:从局部变量 1 加载 int 值
  • putfield:设置对象的 x 字段(y 字段同理)
  • 最后是 return 指令

每行 Java 代码都会被编译成对应的字节码指令,而 Javassist 让操作这些字节码变得异常简单。🚀

4. 生成 Java 类

Javassist 可以用来动态生成新的 Java 类文件。比如我们要创建一个实现 java.lang.Cloneable 接口的 JavassistGeneratedClass 类,并添加一个 int 类型的 id 字段:

ClassFile cf = new ClassFile(
  false, "com.baeldung.JavassistGeneratedClass", null);
cf.setInterfaces(new String[] {"java.lang.Cloneable"});

FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);

生成类文件后,我们可以验证 id 字段是否存在:

ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
 
assertEquals(fields[0].getName(), "id");

5. 加载类的字节码指令

如果需要加载现有类方法的字节码指令,可以通过 CodeAttribute 获取特定方法的字节码,再用 CodeIterator 遍历所有指令。以 Point 类的 move() 方法为例:

ClassPool cp = ClassPool.getDefault();
ClassFile cf = cp.get("com.baeldung.javasisst.Point")
  .getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();

List<String> operations = new LinkedList<>();
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    operations.add(Mnemonic.OPCODE[op]);
}

assertEquals(operations,
  Arrays.asList(
  "aload_0", 
  "iload_1", 
  "putfield", 
  "aload_0", 
  "iload_2",  
  "putfield", 
  "return"));

通过聚合字节码到操作列表,我们就能看到 move() 方法的所有指令。👆

6. 向现有类字节码添加字段

假设要给现有类的字节码添加一个 int 类型字段,可以用 ClassPool 加载类并添加字段:

ClassFile cf = ClassPool.getDefault()
  .get("com.baeldung.javasisst.Point").getClassFile();

FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);

通过反射验证 id 字段是否添加成功:

ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
List<String> fieldsList = Stream.of(fields)
  .map(Field::getName)
  .collect(Collectors.toList());
 
assertTrue(fieldsList.contains("id"));

7. 向类字节码添加构造器

我们可以通过 addInvokespecial() 方法给现有类添加构造器。例如添加一个无参构造器,调用 java.lang.Object<init> 方法:

ClassFile cf = ClassPool.getDefault()
  .get("com.baeldung.javasisst.Point").getClassFile();
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);

MethodInfo minfo = new MethodInfo(
  cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);

遍历字节码验证新构造器是否存在:

CodeIterator ci = code.toCodeAttribute().iterator();
List<String> operations = new LinkedList<>();
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    operations.add(Mnemonic.OPCODE[op]);
}

assertEquals(operations,
  Arrays.asList("aload_0", "invokespecial", "return"));

8. 总结

本文介绍了 Javassist 库的核心功能,展示了如何:

  • 从 Java 代码生成类文件
  • 对现有 Java 类进行字节码操作

所有示例代码可在 GitHub 项目 中找到(Maven 项目,开箱即用)。💡

踩坑提示:字节码操作虽强大,但需谨慎处理,避免破坏类结构或引入运行时错误!


原始标题:Introduction to Javassist