Руководство по манипулированию байт-кодом Java с помощью ASM

1. Вступление

В этой статье мы рассмотрим, как использовать библиотеку http://asm.ow2.org/ для управления существующим классом Java путем добавления полей, добавления методов и изменения поведения существующих методов.

2. зависимости

Нам нужно добавить зависимости ASM в наш pom.xml :

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>6.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-util</artifactId>
    <version>6.0</version>
</dependency>

Мы можем получить последние версии asm и asm-util из Maven. Центральный.

3. Основы API ASM

API ASM предоставляет два стиля взаимодействия с классами Java для преобразования и генерации: на основе событий и на основе дерева.

3.1. API на основе событий

Этот API в значительной степени основан на шаблоне Visitor и по ощущению похож на модель синтаксического анализа SAX ** обработки документов XML. В его основе лежат следующие компоненты:

  • ClassReader - помогает читать файлы классов и является началом

трансформируя класс ** ClassVisitor - предоставляет методы, используемые для преобразования класса

после прочтения необработанных файлов классов ** ClassWriter - используется для вывода конечного продукта класса

преобразование

Именно в ClassVisitor у нас есть все методы посетителя, которые мы будем использовать для прикосновения к различным компонентам (полям, методам и т. Д.) Данного Java-класса. Мы делаем это путем предоставления подкласса ClassVisitor для реализации любых изменений в данном классе.

В связи с необходимостью сохранения целостности выходного класса относительно Java-соглашений и результирующего байт-кода, этот класс требует строгого порядка, в котором его методы должны вызываться для генерации правильного вывода.

Методы ClassVisitor в API на основе событий вызываются в следующем порядке:

visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )**
( visitInnerClass | visitField | visitMethod )**
visitEnd

3.2. API на основе дерева

Этот API является более объектно-ориентированным API и аналогичен модели JAXB обработки XML-документов.

Он все еще основан на API, основанном на событиях, но в нем представлен корневой класс ClassNode . Этот класс служит точкой входа в структуру класса.

4. Работа с основанным на событиях ASM API

Мы изменим класс java.lang.Integer с помощью ASM. И нам нужно понять фундаментальную концепцию: класс ClassVisitor содержит все необходимые методы посетителя для создания или изменения всех частей класса

Нам нужно только переопределить необходимый метод посетителя для реализации наших изменений. Начнем с настройки обязательных компонентов:

public class CustomClassWriter {

    static String className = "java.lang.Integer";
    static String cloneableInterface = "java/lang/Cloneable";
    ClassReader reader;
    ClassWriter writer;

    public CustomClassWriter() {
        reader = new ClassReader(className);
        writer = new ClassWriter(reader, 0);
    }
}

Мы используем это как основу для добавления интерфейса Cloneable в класс Integer , а также добавляем поле и метод.

4.1. Работа с полями

Давайте создадим нашего ClassVisitor , который мы будем использовать для добавления поля в класс Integer :

public class AddFieldAdapter extends ClassVisitor {
    private String fieldName;
    private String fieldDefault;
    private int access = org.objectweb.asm.Opcodes.ACC__PUBLIC;
    private boolean isFieldPresent;

    public AddFieldAdapter(
      String fieldName, int fieldAccess, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
        this.fieldName = fieldName;
        this.access = fieldAccess;
    }
}

Далее, давайте переопределим метод visitField , где мы сначала проверим, существует ли уже поле, которое мы планируем добавить, и установим флаг для указания статуса .

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

Этот метод также позволяет нам изменять видимость или тип существующих полей :

@Override
public FieldVisitor visitField(
  int access, String name, String desc, String signature, Object value) {
    if (name.equals(fieldName)) {
        isFieldPresent = true;
    }
    return cv.visitField(access, name, desc, signature, value);
}

Сначала мы проверяем флаг, установленный в более раннем методе visitField , и снова вызываем метод visitField , на этот раз предоставив имя, модификатор доступа и описание. Этот метод возвращает экземпляр FieldVisitor.

  • VisitEnd метод является последним методом, вызываемым в порядке методов посетителя. Это рекомендуемая позиция для выполнения логики вставки поля ** .

Затем нам нужно вызвать метод visitEnd для этого объекта, чтобы сообщить, что мы закончили посещение этого поля:

@Override
public void visitEnd() {
    if (!isFieldPresent) {
        FieldVisitor fv = cv.visitField(
          access, fieldName, fieldType, null, null);
        if (fv != null) {
            fv.visitEnd();
        }
    }
    cv.visitEnd();
}
  • Важно быть уверенным, что все используемые компоненты ASM взяты из пакета org.objectweb.asm ** - многие библиотеки используют библиотеку ASM для внутреннего использования, и IDE могут автоматически вставлять входящие в комплект библиотеки ASM.

Теперь мы используем наш адаптер в методе addField , получая преобразованную версию java.lang.Integer с нашим добавленным полем:

public class CustomClassWriter {
    AddFieldAdapter addFieldAdapter;
   //...
    public byte[]addField() {
        addFieldAdapter = new AddFieldAdapter(
          "aNewBooleanField",
          org.objectweb.asm.Opcodes.ACC__PUBLIC,
          writer);
        reader.accept(addFieldAdapter, 0);
        return writer.toByteArray();
    }
}

Мы переопределили методы visitField и visitEnd .

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

4.2. Работа с методами

Генерация целых методов в ASM API более сложна, чем другие операции в классе. Это включает в себя значительное количество низкоуровневых манипуляций с байт-кодом и, как следствие, выходит за рамки данной статьи.

Однако для большинства практических целей мы можем либо изменить существующий метод, чтобы сделать его более доступным (возможно, сделать его общедоступным, чтобы его можно было переопределить или перегрузить), либо изменить класс, чтобы сделать его расширяемым .

Давайте сделаем метод toUnsignedString общедоступным:

public class PublicizeMethodAdapter extends ClassVisitor {
    public PublicizeMethodAdapter(int api, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
    }
    public MethodVisitor visitMethod(
      int access,
      String name,
      String desc,
      String signature,
      String[]exceptions) {
        if (name.equals("toUnsignedString0")) {
            return cv.visitMethod(
              ACC__PUBLIC + ACC__STATIC,
              name,
              desc,
              signature,
              exceptions);
        }
        return cv.visitMethod(
          access, name, desc, signature, exceptions);
   }
}

Как и в случае модификации поля, мы просто перехватываем метод посещения и изменяем параметры, которые нам нужны .

В этом случае мы используем модификаторы доступа в пакете org.objectweb.asm.Opcodes , чтобы изменить видимость метода . Затем мы подключаем наш ClassVisitor :

public byte[]publicizeMethod() {
    pubMethAdapter = new PublicizeMethodAdapter(writer);
    reader.accept(pubMethAdapter, 0);
    return writer.toByteArray();
}

4.3. Работа с классами

В том же ключе, что и при модификации методов, мы модифицируем классы, перехватывая соответствующий метод посетителя В этом случае мы перехватываем visit , который является самым первым методом в иерархии посетителей:

public class AddInterfaceAdapter extends ClassVisitor {

    public AddInterfaceAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }

    @Override
    public void visit(
      int version,
      int access,
      String name,
      String signature,
      String superName, String[]interfaces) {
        String[]holding = new String[interfaces.length + 1];
        holding[holding.length - 1]= cloneableInterface;
        System.arraycopy(interfaces, 0, holding, 0, interfaces.length);
        cv.visit(V1__8, access, name, signature, superName, holding);
    }
}

Мы переопределяем метод visit , чтобы добавить интерфейс Cloneable в массив интерфейсов, которые будут поддерживаться классом Integer . Мы подключаем это, как и все другие виды использования наших адаптеров.

5. Использование модифицированного класса

Итак, мы изменили класс Integer . Теперь нам нужно иметь возможность загружать и использовать модифицированную версию класса.

В дополнение к простой записи вывода writer.toByteArray на диск в виде файла класса, есть несколько других способов взаимодействия с нашим настроенным классом Integer .

5.1. Использование TraceClassVisitor

Библиотека ASM предоставляет служебный класс TraceClassVisitor , который мы будем использовать для анализа измененного класса . Таким образом, мы можем подтвердить, что наши изменения произошли .

Поскольку TraceClassVisitor является ClassVisitor , мы можем использовать его в качестве замены для стандартного ClassVisitor :

PrintWriter pw = new PrintWriter(System.out);

public PublicizeMethodAdapter(ClassVisitor cv) {
    super(ASM4, cv);
    this.cv = cv;
    tracer = new TraceClassVisitor(cv,pw);
}

public MethodVisitor visitMethod(
  int access,
  String name,
  String desc,
  String signature,
  String[]exceptions) {
    if (name.equals("toUnsignedString0")) {
        System.out.println("Visiting unsigned method");
        return tracer.visitMethod(
          ACC__PUBLIC + ACC__STATIC, name, desc, signature, exceptions);
    }
    return tracer.visitMethod(
      access, name, desc, signature, exceptions);
}

public void visitEnd(){
    tracer.visitEnd();
    System.out.println(tracer.p.getText());
}

Здесь мы адаптировали ClassVisitor , который мы передали нашему ранее PublicizeMethodAdapter , с помощью TraceClassVisitor .

Все посещения теперь будут выполняться с нашим трассировщиком, который затем может распечатать содержимое преобразованного класса, показывая любые изменения, которые мы внесли в него.

В то время как в документации ASM говорится, что TraceClassVisitor может распечатывать в PrintWriter , который передается конструктору, это не работает должным образом в последней версии ASM.

К счастью, у нас есть доступ к базовому принтеру в классе, и мы смогли вручную распечатать текстовое содержимое трассировщика в нашем переопределенном методе visitEnd .

5.2. Использование Java Instrumentation

Это более элегантное решение, которое позволяет нам работать с JVM на более близком уровне через Instrumentation. .

Чтобы использовать класс java.lang.Integer , мы напишем агент, который будет настроен как параметр командной строки для JVM . Агент требует двух компонентов:

  • Класс, который реализует метод с именем premain

  • Реализация

ClassFileTransformer , в котором мы условно предоставим измененную версию нашего класса

public class Premain {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[]transform(
              ClassLoader l,
              String name,
              Class c,
              ProtectionDomain d,
              byte[]b)
              throws IllegalClassFormatException {
                if(name.equals("java/lang/Integer")) {
                    CustomClassWriter cr = new CustomClassWriter(b);
                    return cr.addField();
                }
                return b;
            }
        });
    }
}

Теперь мы определим наш класс реализации premain в файле манифеста JAR с помощью плагина Maven jar:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <archive>
            <manifestEntries>
                <Premain-Class>
                    com.baeldung.examples.asm.instrumentation.Premain
                </Premain-Class>
                <Can-Retransform-Classes>
                    true
                </Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Сборка и упаковка нашего кода до сих пор создает банку, которую мы можем загрузить как агент. Чтобы использовать наш настроенный класс Integer в гипотетическом « YourClass.class »:

java YourClass -javaagent:"/path/to/theAgentJar.jar"

6. Заключение

В то время как мы реализовали наши преобразования здесь индивидуально, ASM позволяет нам объединять несколько адаптеров вместе для достижения сложных преобразований классов.

В дополнение к базовым преобразованиям, которые мы рассмотрели здесь, ASM также поддерживает взаимодействия с аннотациями, обобщениями и внутренними классами.

Мы видели некоторые возможности библиотеки ASM - она ​​снимает множество ограничений, с которыми мы могли бы столкнуться при использовании сторонних библиотек и даже стандартных классов JDK.

ASM широко используется под капюшоном некоторых из самых популярных библиотек (Spring, AspectJ, JDK и т. Д.), Чтобы выполнять много «волшебства» на лету.

Вы можете найти исходный код этой статьи в проекте GitHub .