Углубленный ASM в нижней части платформы с открытым исходным кодом

Java

Что такое АСМ?

ASM — это среда обработки байт-кода Java. Его можно использовать для динамического создания классов или для улучшения функциональности существующих классов. ASM может напрямую генерировать бинарные файлы классов или динамически изменять поведение классов перед их загрузкой в ​​виртуальную машину Java. Классы Java хранятся в строго отформатированных файлах .class, метаданных которых достаточно для разрешения всех элементов класса: имени класса, методов, свойств и байт-кода Java (инструкций). После того, как ASM прочитает информацию из файлов классов, он может изменить поведение класса, проанализировать информацию о классе и даже создать новые классы в соответствии с требованиями пользователя.

Зачем динамически генерировать классы Java?

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

Почему выбирают АСМ?

Самый прямой способ изменить класс Java — это напрямую переписать файл класса. Спецификация Java детализирует формат файлов классов, и непосредственное редактирование байт-кода действительно может изменить поведение класса Java. По сей день все еще есть некоторые эксперты по Java, которые используют самые примитивные инструменты, редакторы, такие как UltraEdit, для работы с файлами классов. Да, это самый прямой метод, но он требует от пользователя знакомства с форматом файла класса Java: тщательно рассчитайте смещение изменяемой функции относительно заголовка файла и пересчитайте контрольную сумму файла класса , чтобы передать механизм безопасности виртуальной машины Java.

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

Программирование с помощью ASM

ASM предоставляет два API:

  1. CoreAPI (ClassVisitor, MethodVisitor и т. д.)
  2. TreeAPI (узел класса, узел метода и т. д.)

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

В следующем примере используется метод CoreAPI.

Добавьте Мейвена:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>5.0.4</version>
</dependency>

Он относительно стабилен в использовании и использует больше версии 5.0.4.

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

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

Сначала определите класс, который мы хотим улучшить:

package com.zjz;

import java.util.Random;

/**
 * @author zhaojz created at 2019-08-22 10:49
 */
public class Student {
    public String name;

    public void studying() throws InterruptedException {
        System.out.println(this.name+"正在学习...");
        Thread.sleep(new Random().nextInt(5000));
    }
}

Затем сначала определите ClassReader:

ClassReader classReader = new ClassReader("com.zjz.Student");

Затем определите ClassWriter:

 ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);

ClassWriter.COMPUTE_MAXS указывает на автоматический расчет размера локальной переменной и стека операндов. Чтобы узнать больше о других вариантах, см.:asm.ow2.io

Затем начните формально обращаться к классу:

//通过ClassVisitor访问Class(匿名类的方式,可以自行定义为一个独立的类)
//ASM5为JVM字节码指令操作码
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
	//声明一个全局变量,表示增强后生成的子类的父类
   String enhancedSuperName;
   @Override
   public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
   //拼接需要生成的子类的类名:Student$EnhancedByASM
   String enhancedName = name+"$EnhancedByASM";
   //将Student设置为父类
   enhancedSuperName = name;
   super.visit(version, access, enhancedName, signature, enhancedSuperName, interfaces);
   }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    //这里是演示字段访问
    System.out.println("Field:" + name);
    return super.visitField(access, name, desc, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    System.out.println("Method:" + name);
    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
    MethodVisitor wrappedMv = mv;
    //判断当前读取的方法
    if (name.equals("studying")) {
    //如果是studying方法,则包装一个方法的Visitor
    wrappedMv = new StudentStudyingMethodVisitor(Opcodes.ASM5, mv);
    }else if(name.equals("<init>")){
    //如果是构造方法,处理子类中父类的构造函数调用
    wrappedMv = new StudentEnhancedConstructorMethodVisitor(Opcodes.ASM5, mv,enhancedSuperName);
    }
    return wrappedMv;
    }
};

Затем сосредоточьтесь на MethodVisitor:

//Studying方法的Visitor
static class StudentStudyingMethodVisitor extends MethodVisitor{

    public StudentStudyingMethodVisitor(int i, MethodVisitor methodVisitor) {
    	super(i, methodVisitor);
    }

	//MethodVisitor 中定义了不同的visitXXX()方法,代表的不同的访问阶段。
	//visitCode表示刚刚进入方法。
    @Override
    public void visitCode() {
    	//添加一行System.currentTimeMillis()调用
        visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        //并且将其存储在局部变量表内位置为1的地方
        visitVarInsn(Opcodes.LSTORE, 1);
        //上面两个的作用就是在Studying方法的第一行添加 long start = System.currentTimeMillis()
    }

	//visitInsn 表示访问进入了方法内部
    @Override
    public void visitInsn(int opcode) {
    	//通过opcode可以得知当前访问到了哪一步,如果是>=Opcodes.IRETURN && opcode <= Opcodes.RETURN 表明方法即将退出
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)){
        	//加载局部变量表中位置为1的数据,也就是start的数据,并传入给下面的方法
            visitVarInsn(Opcodes.LLOAD, 1);
            //然后调用自定义的一个工具方法,用来输出耗时
            visitMethodInsn(Opcodes.INVOKESTATIC, "com/zjz/Before", "end", "(J)V", false);
        }
        super.visitInsn(opcode);
    }


}

static class StudentEnhancedConstructorMethodVisitor extends MethodVisitor{
	//定义一个全局变量记录父类名称
    private String superClassName;
    public StudentEnhancedConstructorMethodVisitor(int i, MethodVisitor methodVisitor,String superClassName) {
        super(i, methodVisitor);
        this.superClassName = superClassName;
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean b) {
    	//当开始初始化构造函数时,先访问父类构造函数,类似源码中的super()
        if (opcode==Opcodes.INVOKESPECIAL && name.equals("<init>")){
        	owner = superClassName;
        }
        super.visitMethodInsn(opcode, owner, name, desc, b);
    }
}

В настоящее время ClassVisitor не имеет ввода данных, а только определяет вывод данных new ClassVisitor(Opcodes.ASM5, classWriter), поэтому также необходимо:

classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);

На этом процесс чтения, доступа, изменения и вывода класса завершен.

Внимательные зрители узнают, где выход? Как получить доступ к вновь созданному классу? Итак, нам нужно определить ClassLoader для загрузки нашего сгенерированного класса:

 static class StudentClassLoader extends ClassLoader{
        public Class defineClassFromClassFile(String className,byte[] classFile) throws ClassFormatError{
            return defineClass(className, classFile, 0, classFile.length);
        }
    }

Затем получите массив байтов вновь сгенерированного класса через ClassWriter и загрузите его в JVM:

 byte[] data = classWriter.toByteArray();
 Class subStudent = classLoader.defineClassFromClassFile("com.zjz.Student$EnhancedByASM", data);

На этом генерация класса завершена.Приведенный выше код выполняет очень простую вещь: записывает время обучения.

В заключение:

Три основные вещи ASM CoreAPI — это ClassReader, Visitor и ClassWriter, которые связаны через модель цепочки ответственности.

Посетитель получает доступ к методам, полям и другим атрибутам через режим посетителя. Если вам нужно изменить метод или поле, вам нужно только передать исходный посетитель для переноса.

Чтобы понять, как кодировать, требуется понимание инструкций байт-кода, связанных с JVM, и OpCode, связанного с ASM.

ASM Bytecode Outline 2017

Но как запомнить столько инструкций, опкодов и символов? Например, в приведенном выше коде:

visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
visitVarInsn(Opcodes.LSTORE, 1);

Opcodes.INVOKESTATIC, Opcodes.LSTORE, ()J, вы чувствуете головокружение, просто глядя на них? На самом деле, помимо практики, вы также можете использовать инструменты.

Если вы используете IDEA, вы можете установить плагин ASM Bytecode Outline 2017. Затем щелкните правой кнопкой мыши исходный файл и выберите Показать схему байт-кода, вы увидите следующее представление:

image.png

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

Ознакомьтесь с полным исходным кодом примера:asm_demo

Использованная литература:

asm.ow2.io/

Woohoo. IBM.com/developer Я…

nuggets.capable/post/684490…