Open clankill3r opened 4 years ago
I cannot see any good algorithm to determine whether these arguments are valid or not. Can you?
Sorry I don't understand the question.
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.
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.
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.
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.
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.
I would but some source in between that has recognisable bytecode, that you later can search for and remove.
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.
As the developer of the library @chibash said, it's technically challenging to accomplish what the OP wanted. However, there's a workaround.
$method()
. Optionally set its modifier to private
.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.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
@blackr1234 Thank you. I was about to work on this in the upcoming days. Your post comes at a welcome time 👍
@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.
This method would be really welcome:
void insertBeforeAndAfter(String srcBefore, String sourceAfter)
Cause this is not possible for example"
While it would have been valid code.