So I have been using Javassist a bit lately, and I have run into a question I haven't been able to find an answer to. The insertAt method of CtMethod allows you to insert code at a specific line number, but does it overwrite that line or keep it, and how do I make it do the opposite of what it does by default? I have an application which modifies source just before runtime with Javassist, based on 'hooks' in an XML file. I want to make it so that a line can be overridden, or a line can be placed above the line instead of overriding it. Obviously there are hackish ways to do that, but I'd rather use a proper way.
问题:
回答1:
The easy part
The method insertAt(int lineNumber, String src) present in CtMethod object allows injecting the code written in src before the code that was in the given line.
For instance, take the following (simple) example program:
public class TestSubject {
public static void main(String[] args) {
TestSubject testSubject = new TestSubject();
testSubject.print();
}
private void print() {
System.out.println("One"); // line 9
System.out.println("Two"); // line 10
System.out.println("Three"); // line 11
}
}
By simply coding (keep in mind that method variable must be the CtMethod representation of print method):
// notice that I said line 10, which is where the sysout of "two" is
method.insertAt(10, true, "System.out.println(\"one and an half\");");
Will inject a new sysout instruction in the class. The output of the new class will be:
one
one and an half
two
three
The hard part
Javassist does not provide an easy way to remove a line of code, so if you really want to replace it you'll have no choice than hack your way through.
How to do it? Well, let me introduce you to your new friend (if you don't know it yet), the CodeAttribute object.
The CodeAttribute object is responsible for holding the bytecode that represents the method flow besides that code attribute also has another attribute called LineNumberAttribute which helps you map the line numbers into the bytecode array. So summing up this object has everything you need!
The idea in the following example is quite simple. Relate the bytes in bytecode array with the line that should be removed and substitute the bytes by a no operation code.
Once again, method is the CtMethod representation of method print
// let's erase the sysout "Two"
int lineNumberToReplace = 10;
// Access the code attribute
CodeAttribute codeAttribute = method.getMethodInfo().getCodeAttribute();
// Access the LineNumberAttribute
LineNumberAttribute lineNumberAttribute = (LineNumberAttribute) codeAttribute.getAttribute(LineNumberAttribute.tag);
// Index in bytecode array where the instruction starts
int startPc = lineNumberAttribute.toStartPc(lineNumberToReplace);
// Index in the bytecode array where the following instruction starts
int endPc = lineNumberAttribute.toStartPc(lineNumberToReplace+1);
System.out.println("Modifying from " + startPc + " to " + endPc);
// Let's now get the bytecode array
byte[] code = codeAttribute.getCode();
for (int i = startPc; i < endPc; i++) {
// change byte to a no operation code
code[i] = CodeAttribute.NOP;
}
Running this modification in the original TestSubject class, would result in an injected class with the following output:
one
three
Summing Up
When you have the need to add a line and still keeping the existing one, you just need to use the example given in the easy part if you want to replace the line, you have to first remove the existing line using the example given in the hard part and then inject the new line using the 1st example.
Also keep in mind that in the examples I assumed you were already comfortable with the basics of javassist showing you only the juicy parts, instead of the all deal. That's why, for instance, in the examples there is no ctClass.writeFile... you still need to do it, I just left it out because I do expect you should know you have to do it.
If you need any extra help in the code examples, just ask. I'll be glad to help.