Введение в Javassist

Введение в Javassist

1. обзор

В этой статье мы рассмотрим библиотекуJavasisst (Java Programming Assistant).

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

2. Maven Dependency

Чтобы добавить библиотеку Javassist в наш проект, нам нужно добавитьjavassist в наш pom:


    org.javassist
    javassist
    ${javaassist.version}



    3.21.0-GA

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.example.javasisst.Point {
  public com.example.javasisst.Point(int, int);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()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; a large number of them are available.

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

  • Инструкцияaload_0 загружает ссылку в стек из локальной переменной 0

  • iload_1 загружает значение типа int из локальной переменной 1

  • putfield устанавливает полеx нашего объекта. Для поляy все операции аналогичны

  • Последняя инструкция -return

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

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

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

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

ClassFile cf = new ClassFile(
  false, "com.example.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.example.javasisst.Point")
  .getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();

List 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.example.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 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.example.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 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 project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.