Modifying Java Virtual Machine Bytecode

    This post is a continuation of the article on the bytecode of the Java virtual machine, and we believe that the reader has an idea of ​​its structure. The most common bytecode modification library is the object web ASM framework . Most of the high-level libraries, in particular cglib, are built on it.

    The ASM library has two API options. To better imagine the difference between them, we draw the following analogy. Class is a kind of tree. Its root is the class itself. Variables, methods, subclasses are its leaves. Instructions are leaves of methods. Thus, it is possible to draw a parallel with XML and its two types of parsers. The first version of the Core API is similar to the SAX parser. When you need to read, create, or make changes, a traversal of the class representation tree is made. The second option (Tree API) works on the DOM parser trailer. First, a presentation tree is built, and then the necessary manipulations are performed with it. Obviously, the first version of the API is less resource-intensive, more suitable for making small changes. The second requires more resources, but also provides more flexible options. We will consider only the first version of the API.



    To create a class using the Core API, you need to bypass the tree of its presentation. Consider a more specific example. We want to profile the following class. For example, get information about the execution time of the run method. To do this, create an heir. And here is how you can create it using ASM. Thus, we got an array of bytes in which the structure of the new class is stored. Now we need to load this new class. To do this, you need your own ClassLoader, since the standard method for loading a class has the protected modifier. And here is how to do it. Thus, we can create a utility for quick profiling, which with the help of reflection receives information and using it creates the desired class. Libraries implementing AOP are built on a similar trailer.

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

    }




    public class Test extends Example {

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




    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, "", "()V", null, null);
    methodVisitor.visitCode();
    methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
    methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/example/Example", "", "()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();




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




    Consider another example. Suppose we need to test a class that calls System.currentTimeMillis (). To do this, it suffices to learn how to replace the call of currentTimeMillis () with a call to another static method. We act like this: we read the class, bypassing its tree and changing the method call where necessary. Loading a modified class has some features. In jvm, classes are defined not only by their name, but also by the ClassLoader that was used to load it. A class loaded using MyClassLoader will have a different type than the class loaded by ClassLoder by default. Thus calling the method, we need to use reflection.


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




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


    You can also load both the Test and Example classes using MyClassLoader, and if you call the run method on the Test class, the changed method will be called on the Example class.

    A working example can be taken from here .

    Modifying Java Virtual Machine Bytecode

    Also popular now: