Рубрики
Без рубрики

Руководство по java Bytecode Манипуляция с ASM

Узнайте, как изменить существующий класс Java, манипулируя его ютекодом с помощью инфраструктуры ASM.

Автор оригинала: baeldung.

1. Введение

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

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

Мы должны добавить зависимости ASM к нашей пом.xml :


    org.ow2.asm
    asm
    6.0


    org.ow2.asm
    asm-util
    6.0

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

3. Основы ASM API

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

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

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

  • КлассЧитатель – помогает читать файлы классов и является началом преобразования класса
  • КлассВизитор — предоставляет методы, используемые для преобразования класса после прочтения необработанных файлов класса
  • КлассРайтер – используется для вывода конечного продукта преобразования класса

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

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

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

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

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

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

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

4. Работа с API ASM на основе событий

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

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

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

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

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

Давайте создадим нашу КлассВизитор которые мы будем использовать, чтобы добавить поле в Интегер класс:

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 метод снова, на этот раз предоставление имени, модификатора доступа и описания. Этот метод возвращает экземпляр ПолевойВизитор.

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 внутренне, а IDEs могут автоматически вставлять в комплекте библиотеки ASM.

Теперь мы используем наш адаптер в добавитьФилд метод, получение преобразованной версии 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. Работа с методами

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

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

Давайте сделают метод 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 пакет для изменить видимость метода . Затем мы подключаем наши КлассВизитор :

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

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

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

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

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

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

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

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

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

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

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

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

То, что мы сделали здесь, чтобы адаптировать КлассВизитор что мы перешли к нашим предыдущим PublicizeMethodAdapter с TraceClassVisitor .

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

Хотя в документации АСМ говорится, что TraceClassVisitor может распечатать на РаспечататьПисач это поставляется конструктору, это, кажется, не работает должным образом в последней версии ASM.

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

5.2. Использование инструментов Java

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

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

  • Класс, который реализует метод под названием премейн
  • Реализация КлассФилеТрансформер в котором мы будем условно поставлять модифицированную версию нашего класса
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;
            }
        });
    }
}

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


    org.apache.maven.plugins
    maven-jar-plugin
    2.4
    
        
            
                
                    com.baeldung.examples.asm.instrumentation.Premain
                
                
                    true
                
            
        
    

Строительство и упаковка нашего кода до сих пор производит банку, которую мы можем загрузить в качестве агента. Чтобы использовать наши индивидуальные Интегер класс в гипотетическом " Вашкласс.class ":

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

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

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

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

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

ASM широко используется под капотом некоторых из самых популярных библиотек (Весна, AspectJ, JDK и т.д.), чтобы выполнить много "волшебства" на лету.

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