Pull to refresh

Модификация байт-кода виртуальной машины Java

Reading time 7 min
Views 12K
Данный пост является продолжением статьи о байт-коде виртуальной машины Java, и мы считаем, что читатель имеет представление о его структуре. Наиболее распространенной библиотекой для модификации байт-кода является фрейморк ASM от object web. На нем построено большинство высокоуровневых библиотек, в частности cglib.

Библиотека ASM имеет два варианта API. Что бы лучше представить отличие между ними, проведем следующую аналогию. Класса это некое дерево. Корень его- сам класс. Переменные, методы, подклассы это его листья. Инструкции — листья методов. Таким образом можно провести параллель с XML и двумя типами его парсеров. Первый вариант Core API похож на SAX парсер. Когда нужно прочитать, создать или внести изменения, делается обход дерева представления класса. Второй вариант (Tree API) работает по прицепу DOM парсера. Сначала строиться дерево представления, а затем с ним производиться необходимые манипуляции. Очевидно, что первый вариант API менее ресурсоемкий, более подходящей для внесения небольших изменений. Второй требует больше ресурсов, но и дает более гибкие возможности. Мы рассмотрим только первый вариант API.



Что бы создать класса с помощью Core API, нужно обойти дерево его представления. Рассмотрим более конкретный пример. Мы хоти профилировать следующий класс

public class Example {

    public void run(String name) {
        try {
            Thread.sleep(5);
            System.out.println("Currennt time is " + new Date(System.currentTimeMillis()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name);
    }

}


Например получать информацию о времени выполнения метода run. Для этого создадим наследника.

public class Test extends Example {

    @Override
    public void run(String name)  {
        long l = System.currentTimeMillis();
        super.run(name);
        System.out.println((System.currentTimeMillis() - l));
    }
}


А вот как можно создать его используя ASM.

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC, "org/Test", null, "org/example/Example", null);
cw.visitField(Opcodes.ACC_PRIVATE, "name", "Ljava/lang/String;",null, null).visitEnd();

MethodVisitor methodVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/example/Example", "<init>", "()V");
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();

methodVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "run", "(Ljava/lang/String;)V", null, null);
methodVisitor.visitCode();
methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
methodVisitor.visitVarInsn(Opcodes.LSTORE, 2);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/example/Example", "run", "(Ljava/lang/String;)V");
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
methodVisitor.visitVarInsn(Opcodes.LLOAD, 2);
methodVisitor.visitInsn(Opcodes.LSUB);
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V");
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(5, 4);
methodVisitor.visitEnd();

cw.visitEnd();
return cw.toByteArray();


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

class MyClassLoader extends ClassLoader {

    public Class defineClass(String name, byte[] b) {
        return defineClass(name, b, 0, b.length);
    }
}


А вот как это можно сделать

MyClassLoader myClassLoader = new MyClassLoader();
Class bClass = myClassLoader.defineClass("org.Test", Generator.getBytecodeForClass());

Constructor constructor = bClass.getConstructor();
Object o = constructor.newInstance();

Example e = (Example) o;
e.run("test");


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

Рассмотрим другой пример. Допустим нам нужно протестировать класс, который вызывает System.currentTimeMillis(). Для этого нам достаточно научиться заменять вызов currentTimeMillis(), на вызов другого статического метода. Действуем так: читаем класс, обходя его дерево и где нужно изменяя вызов метода.


ClassReader cr = new ClassReader("org.example.Example");
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);

ClassVisitor cv = new ReplaceStaticMethodClassAdapter(cw);
cr.accept(cv, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);

return cw.toByteArray();

//----------------------------------------------------------------------------


private static class ReplaceStaticMethodClassAdapter extends ClassAdapter{

    public RepaleStaticMethodClassAdapter(ClassVisitor classVisitor) {
        super(classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
        return new RepalceStaticMethodAdapter(super.visitMethod(i, s, s1, s2, strings));
    }

    private class RepalceStaticMethodAdapter extends MethodAdapter{
        public RepalceStaticMethodAdapter(MethodVisitor methodVisitor) {
            super(methodVisitor);
        }

        @Override
        public void visitMethodInsn(int i, String s, String s1, String s2) {
            if(== Opcodes.INVOKESTATIC &&  "java/lang/System".equals(s) && "currentTimeMillis".equals(s1) && "()J".equals(s2)){
                super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/example/Generator", "myTime", "()J");
            }  else{
                super.visitMethodInsn(i, s, s1, s2);
            }
        }
    }
}


Загрузкой модифицированного класса имеет некоторые особенности. В jvm классы определяются не только своим именем, но и еще ClassLoader который был использован при его загрузки. Класс загруженный с помощью MyClassLoader будет иметь другой тип, чем тоже класс загруженный ClassLoder по умолчанию. Таким образом вызвать метод, нам понадобится использовать reflection.

MyClassLoader myClassLoader = new MyClassLoader();
Class aClass = myClassLoader.defineClass("org.example.Example", Generator.getModifedClass());
Constructor constructor = aClass.getConstructor();
Object o = constructor.newInstance();
Method method = aClass.getMethod("run", String.class);
method.invoke(o,"test");


Можно также загрузить оба класса Test и Example c помощью MyClassLoader, и если у класса Test вызывать метод run, то у класса Example будет вызывается измененный метод.

Работающий пример можно взять отсюда.

Модификация байт-кода виртуальной машины Java
Tags:
Hubs:
+17
Comments 2
Comments Comments 2

Articles