Search code examples
javajava-8java-bytecode-asm

Java ASM edit class/method that is aleready loaded by JVM


I'm currently trying to edit the bytecode of a java class after it has been loaded by the JVM.

I use Java 8 and ASM 5.0.3. I can't change the command line or the JVM arguments.

Here is a minimal example of what I'm trying to do:

import org.objectweb.asm.*;

// Is in a library
class ExampleObject {
    public void exampleMethod(Object o) {
        System.out.println("Body " + o);
    }
}

public class Example {

    // Comes from the same library
    public static final ExampleObject exampleObject = new ExampleObject();

    public static void main(String[] args) {

        exampleObject.exampleMethod(1);
        // Output:
        // Body 1

        injectAtHead();

        exampleObject.exampleMethod(2);
        // Output:
        // Head 2
        // Body 2
    }

    public static void injectAtHead() {
        // Inject methodToInject at the head of exampleObject#exampleMethod
    }

    public static void methodToInject(Object o) {
        System.out.println("Head " + o);
    }
}

After a lot of research, I came across quite a few topics that talked about dynamicly modifying bytecode with ASM. The problem is that they all talk about modifying the bytecode of a class before it is loaded by the JVM. So, I don't know how I can do this, or even if it's possible at all.


Solution

  • This comment pretty much answers the question.

    After a few adaptations, the solution to the initial problem is:

    import net.bytebuddy.agent.ByteBuddyAgent;
    import org.objectweb.asm.*;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.Instrumentation;
    import java.security.ProtectionDomain;
    
    import static net.bytebuddy.jar.asm.Opcodes.*;
    
    public class Example {
    
        // Also comes from a library
        public static final ExampleObject exampleObject = new ExampleObject();
    
        public static void main(String[] args) {
    
            exampleObject.exampleMethod(1);
            // Output:
            // Body 1
    
            injectAtHead();
    
            exampleObject.exampleMethod(2);
            // Output:
            // Head 2
            // Body 2
        }
    
        public static void injectAtHead() {
            try {
                ByteBuddyAgent.install();
                Instrumentation instrumentation = ByteBuddyAgent.getInstrumentation();
                instrumentation.addTransformer(new SimpleClassFileTransformer(), true);
    
                instrumentation.retransformClasses(ExampleObject.class);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    
    
        public static void methodToInject(Object o) {
            System.out.println("Head " + o);
        }
    
    }
    
    
    class ModifierMethodWriter extends MethodVisitor {
    
        private String methodName;
    
        public ModifierMethodWriter(int api, MethodVisitor mv, String methodName) {
            super(api, mv);
            this.methodName = methodName;
        }
    
        // This is the point we insert the code. Note that the instructions are
        // added right after
        // the visitCode method of the super class. This ordering is very
        // important.
        @Override
        public void visitCode() {
            // invoke methodToInject
            super.visitCode();
            super.visitVarInsn(ALOAD, 1);
            super.visitMethodInsn(INVOKESTATIC, "Example", "methodToInject", "(Ljava/lang/Object;)V", false);
        }
    
    }
    
    // Our class modifier class visitor. It delegate all calls to the super
    // class
    // Only makes sure that it returns our MethodVisitor for every method
    class ModifierClassWriter extends ClassVisitor {
        private int api;
    
        public ModifierClassWriter(int api, ClassWriter cv) {
            super(api, cv);
            this.api = api;
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    
            if (!name.equals("exampleMethod")){
                return super.visitMethod(access, name, desc, signature, exceptions);
            } else {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                ModifierMethodWriter mvw = new ModifierMethodWriter(api, mv, name);
                return mvw;
            }
        }
    
    }
    
    class SimpleClassFileTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain, byte[] classfileBuffer) {
    
            if (!className.equals("ExampleObject")) {
                return classfileBuffer;
            }
            else {
                ClassReader classReader = new ClassReader(classfileBuffer);
    
                final ClassWriter cw = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
    
                // Wrap the ClassWriter with our custom ClassVisitor
                ModifierClassWriter mcw = new ModifierClassWriter(Opcodes.ASM5, cw);
                classReader.accept(mcw, 0);
    
                byte[] byteArray = cw.toByteArray();
    
                return byteArray;
            }
        }
    
    }