Перейти к основному содержимому

Введение в Javassist

· 4 мин. чтения

1. Обзор

В этой статье мы рассмотрим библиотеку Javasisst (помощник по программированию на Java) .

Проще говоря, эта библиотека упрощает процесс управления байт-кодом Java за счет использования высокоуровневого API, чем в JDK.

2. Зависимость от Maven

Чтобы добавить библиотеку 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. Что такое байт-код?

На очень высоком уровне каждый класс Java написан в текстовом формате и скомпилирован в байт-код — набор инструкций, который может обрабатываться виртуальной машиной Java. JVM переводит инструкции байт-кода в инструкции сборки машинного уровня.

Допустим, у нас есть класс 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
}

После компиляции будет создан файл Point.class , содержащий байт-код. Мы можем увидеть байт-код этого класса, выполнив команду javap :

javap -c Point.class

Это напечатает следующий вывод:

public class com.foreach.javasisst.Point {
public com.foreach.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 загружает значение int из локальной переменной 1
  • putfield устанавливает поле x нашего объекта. Все операции аналогичны для поля y
  • Последняя инструкция - возврат

Каждая строка кода Java компилируется в байт-код с соответствующими инструкциями. Библиотека Javassist позволяет относительно легко манипулировать этим байт-кодом.

4. Создание класса Java

Библиотека Javassist может использоваться для создания новых файлов классов Java.

Допустим, мы хотим сгенерировать класс JavassistGeneratedClass , реализующий интерфейс java.lang.Cloneable . Мы хотим, чтобы этот класс имел поле id типа int . ClassFile используется для создания нового файла класса, а FieldInfo используется для добавления нового поля в класс :

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

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

После того, как мы создадим JavassistGeneratedClass.class , мы можем утверждать, что он действительно имеет поле id :

ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();

assertEquals(fields[0].getName(), "id");

5. Загрузка инструкций байт-кода класса

Если мы хотим загрузить инструкции байт-кода уже существующего метода класса, мы можем получить CodeAttribute определенного метода класса. Затем мы можем заставить CodeIterator перебирать все инструкции байт-кода этого метода.

Давайте загрузим все инструкции байт-кода метода move() класса Point :

ClassPool cp = ClassPool.getDefault();
ClassFile cf = cp.get("com.foreach.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 в байт-код существующего класса. Мы можем загрузить этот класс с помощью ClassPoll и добавить в него поле:

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

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

Мы можем использовать отражение, чтобы убедиться, что поле 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. Добавление конструктора в байт-код класса

Мы можем добавить конструктор к существующему классу, упомянутому в одном из предыдущих примеров, с помощью метода addInvokespecial() .

И мы можем добавить конструктор без параметров, вызвав метод <init> из класса java.lang.Object :

ClassFile cf = ClassPool.getDefault()
.get("com.foreach.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, поэтому его должно быть легко импортировать и запускать как есть.