Introduction to Javassist – Javassist简介

最后修改: 2017年 3月 16日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we will be looking at the Javasisst (Java Programming Assistant) library.

在这篇文章中,我们将关注Javasisst(Java编程助手)库。

Simply put, this library makes the process of manipulating Java bytecode simpler by using a high-level API than the one in the JDK.

简单地说,这个库通过使用比JDK中的高级API,使操作Java字节码的过程更加简单。

2. Maven Dependency

2.Maven的依赖性

To add the Javassist library to our project we need to add javassist into our pom:

为了将Javassist库添加到我们的项目中,我们需要将javassist添加到我们的pom中。

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

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

3. What Is the Bytecode?

3.什么是字节码?

At a very high level, every Java class that is written in a plain text format and compiled to bytecode – an instruction set that can be processed by the Java Virtual Machine. The JVM translates bytecode instructions into machine level assembly instructions.

在一个非常高的水平上,每一个以纯文本格式编写的Java类,都被编译成字节码–一个可以被Java虚拟机处理的指令集。JVM将字节码指令翻译成机器级汇编指令。

Let’s say that we have a Point class:

比方说,我们有一个Point 类。

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

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

    // standard constructors/getters/setters
}

After compilation, the Point.class file containing the bytecode will be created. We can see the bytecode of that class by executing the javap command:

编译后,包含字节码的Point.class文件将被创建。我们可以通过执行javap命令看到该类的字节码。

javap -c Point.class

This will print the following output:

这将打印以下输出。

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
}

All those instructions are specified by the Java language; a large number of them are available.

所有这些指令都是由Java语言指定的;有大量的指令可用

Let’s analyze the bytecode instructions of the move() method:

我们来分析一下move()方法的字节码指令。

  • aload_0 instruction is loading a reference onto the stack from the local variable 0
  • iload_1 is loading an int value from the local variable 1
  • putfield is setting a field x of our object. All operations are analogical for field y
  • The last instruction is a return

Every line of Java code is compiled to bytecode with proper instructions. The Javassist library makes manipulating that bytecode relatively easy.

每一行Java代码都被编译成带有适当指令的字节码。Javassist库使操作该字节码变得相对容易。

4. Generating a Java Class

4.生成一个Java类

Javassist library can be used for generating new Java class files.

Javassist库可用于生成新的Java类文件。

Let’s say that we want to generate a JavassistGeneratedClass class that implements a java.lang.Cloneable interface. We want that class to have an id field of int type. The ClassFile is used to create a new class file and FieldInfo is used to add a new field to a class:

假设我们想生成一个JavassistGeneratedClass类,它实现了java.lang.Cloneable接口。我们希望该类有一个id字段,int类型ClassFile用于创建一个新的类文件,FieldInfo用于向类添加一个新的字段

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);

After we create a JavassistGeneratedClass.class we can assert that it actually has an id field:

在我们创建了一个JavassistGeneratedClass.class之后,我们可以断言它实际上有一个id字段。

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

5. Loading Bytecode Instructions of Class

5.加载类的字节码指令

If we want to load bytecode instructions of an already existing class method, we can get a CodeAttribute of a specific method of the class. Then we can get a CodeIterator to iterate over all bytecode instructions of that method.

如果我们想加载一个已经存在的类方法的字节码指令,我们可以得到一个CodeAttribute的类的特定方法。然后我们可以得到一个CodeIterator来迭代该方法的所有字节码指令。

Let’s load all bytecode instructions of the move() method of the Point class:

让我们加载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"));

We can see all bytecode instructions of the move() method by aggregating bytecodes to the list of operations, as shown in the assertion above.

我们可以通过将字节码汇总到操作列表中,看到move()方法的所有字节码指令,如上面的断言所示。

6. Adding Fields to Existing Class Bytecode

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

Let’s say that we want to add a field of int type to the bytecode of the existing class. We can load that class using ClassPoll and add a field into it:

假设我们想在现有类的字节码中添加一个int类型的字段。我们可以使用ClassPoll加载该类,并在其中添加一个字段。

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);

We can use reflection to verify that id field exists on the Point class:

我们可以使用反射来验证id字段是否存在于Point类上。

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. Adding Constructor to Class Bytecode

7.在类的字节码中添加构造函数

We can add a constructor to the existing class mentioned in one of the previous examples by using an addInvokespecial() method.

我们可以通过使用addInvokespecial()方法为前面一个例子中提到的现有类添加一个构造函数。

And we can add a parameterless constructor by invoking a <init> method from java.lang.Object class:

而我们可以通过调用<init>类中的java.lang.Object方法来添加一个无参数构造函数。

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);

We can check for the presence of the newly created constructor by iterating over bytecode:

我们可以通过遍历字节码来检查新创建的构造函数是否存在。

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. Conclusion

8.结论

In this article, we introduced the Javassist library, with the goal of making bytecode manipulation easier.

在这篇文章中,我们介绍了Javassist库,其目的是使字节码操作更容易。

We focused on the core features and generated a class file from Java code; we also made some bytecode manipulation of an already created Java class.

我们专注于核心功能,并从Java代码中生成一个类文件;我们还对一个已经创建的Java类进行了一些字节码操作。

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.

所有这些例子和代码片段的实现都可以在GitHub项目中找到–这是一个Maven项目,所以应该很容易导入并按原样运行。