jboss-javassist / javassist

Java bytecode engineering toolkit
www.javassist.org
Other
4.1k stars 695 forks source link

add a insertBeforeAndAfter #292

Open clankill3r opened 4 years ago

clankill3r commented 4 years ago

This method would be really welcome: void insertBeforeAndAfter(String srcBefore, String sourceAfter)

Cause this is not possible for example"

cm.insertBefore("{");
cm.insertAfter("}");

While it would have been valid code.

chibash commented 4 years ago

I cannot see any good algorithm to determine whether these arguments are valid or not. Can you?

clankill3r commented 4 years ago

Sorry I don't understand the question.

chibash commented 4 years ago

Ah, I intended to say that I didn't know how to implement your idea. Since the source code of the method body is not available, making insertBeforeAndAfter work for arbitrary cases would be more difficult than you expect.

chibash commented 4 years ago

It might be good to add a bit more comment. Javassist modifies the bytecode without its source code as if it virtually allows the users to modify the source code. Unfortunately, this is not real. So many things are difficult to enable although it looks very easy for the users who misconceive that Javassist modifies and compiles the source code to obtain the modified byte code. Your suggestion is the case as long as I can see.

chibash commented 4 years ago

Please note that Javassist never decompiles the bytecode to obtain the source code. The Java bytecode preserves many semantic features in the source code but obtaining the exact source code by decompilation is very difficult. Lot's of corner cases. Reproducing loop structures needs complex algorithms and I don't know the perfect one. etc.

clankill3r commented 4 years ago

I probably know too little, but my idea was to combine the String srcBefore, String sourceAfter into one string, see if that can be compiled into byte code. Then prepend the byte code of the srcBefore part and append the bytecode of the sourceAfter part.

chibash commented 4 years ago

Oh, "Then prepend the byte code of the srcBefore part and append the bytecode of the sourceAfter part." is not easy. How can you split the compiled binary into the before part and the after part? Please remember how the source code is compiled into bytecode. I must say it is not impossible but needs lots of engineering.

clankill3r commented 4 years ago

I would but some source in between that has recognisable bytecode, that you later can search for and remove.

chibash commented 4 years ago

Yes, but the problem is the two bytecode sequences split at that recognizable bytecode would not work unless the local variable numbers (i.e. register numbers) are renumbered, the exception table is updated, the stack map table are reconstructed, etc. These modifications will be possible but would need lots of engineering efforts. We also have to come up with an algorithm to detect whether valid source code is given to insertBeforeAndAfter and report an error when the code is invalid.

blackr1234 commented 4 years ago

As the developer of the library @chibash said, it's technically challenging to accomplish what the OP wanted. However, there's a workaround.

  1. Rename the original method to something like $method(). Optionally set its modifier to private.
  2. Clone the method and replace its method body (written in String). This method will invoke the original method which is now wrapped by your before and after source code. Assign the result to a variable first if the method has a return type.
  3. Call the new method (method signature remains unchanged).

This approach also works when you have another method in the class referencing the original method that you'd rename. Consider the following code. It may not always work and can be missing tons of validations, but if you are not working on a very complex class or doing it dynamically to any method it should just work easily.

package code;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;

public class MainJavassist {
    public static void main(String[] args) throws Exception {

        final ClassPool cp = ClassPool.getDefault();
        final CtClass cc = cp.get("code.Hello");
        insertBeforeAndAfter(
            cc.getDeclaredMethod("sayHello"),
            "{" + "System.out.println(\"Calling the original method.\");",
            "System.out.println(\"Done calling the original method.\");" + "}");
        final Class<?> c = cc.toClass();

        final Hello hello = ((Hello) c.newInstance());
        System.out.println("Result: " + hello.sayHello());
        System.out.println("Say again: " + hello.anotherSayHello());
    }

    private static void insertBeforeAndAfter(CtMethod cm, String before, String after) throws Exception {

        final CtClass cc = cm.getDeclaringClass();
        final String newMethodName = "$" + cm.getName();
        final CtClass returnTypeClass = cm.getReturnType();
        final String returnType = returnTypeClass.getName();

        final String methodBody = "{"
            + (CtClass.voidType.equals(returnTypeClass) ?
                 (
                       before
                     + newMethodName + "();"
                     + after
                 ) : (
                       returnType + " result = null;"
                     + before
                     + "result = " + newMethodName + "();"
                     + after
                     + "return result;"
                 )
             )
            + "}";

        final CtMethod newMethod = CtNewMethod.copy(cm, cc, null);
        cm.setName(newMethodName);
        cm.setModifiers(Modifier.setPrivate(cm.getModifiers()));
        newMethod.setBody(methodBody);
        cc.addMethod(newMethod);
    }
}

class Hello {
    public /* static final */ String sayHello() {
        System.out.println("hello~");
        return "test";
    }

    public String anotherSayHello() {
        System.out.println("\n----- anotherSayHello() -----\n");
        return sayHello();
    }
}

Console output:

Calling the original method.
hello~
Done calling the original method.
Result: test

----- anotherSayHello() -----

Calling the original method.
hello~
Done calling the original method.
Say again: test
clankill3r commented 4 years ago

@blackr1234 Thank you. I was about to work on this in the upcoming days. Your post comes at a welcome time 👍

blackr1234 commented 4 years ago

@blackr1234 Thank you. I was about to work on this in the upcoming days. Your post comes at a welcome time 👍

You're welcome.

I just updated my answer. It turns out that I can just clone a CtMethod and modify some of its parts, such as its method body. By the way, I also find that there's such a method: CtMethod#setWrappedBody(mbody, constParam). Its JavaDoc states that it can:

Replace a method body with a new method body wrapping the given method.

I haven't given it a try but there's a chance it can achieve our goal too.