I have to instrument any given code (without directly changing given code ) at the beginning and end of every thread. Simply speaking , how can I print something at entry and exit points of any thread.
How can I do that using javassist ?
You can do this by creating an ExprEditor and use it to modify MethodCalls that match with start and join of thread objects.
Before we start just let me say that you shouldn't be intimidated by the long post, most of it is just code and once you break things down it's pretty easy to understand!
Let's get busy then...
Imagine you have the following dummy code:
public class GuineaPig {
public void test() throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++)
System.out.println(i);
}
});
t.start();
System.out.println("Sleeping 10 seconds");
Thread.sleep(10 * 1000);
System.out.println("Done joining thread");
t.join();
}
}
When you run this code doing
new GuineaPig().test();
You get an output like (the sleeping system.out may show up in the middle of the count since it runs in the main thread):
Sleeping 10 seconds
0
1
2
3
4
5
6
7
8
9
Done joining thread
Our objective is to create a code injector that will make the output change for the following:
Detected thread starting with id: 10
Sleeping 10 seconds
0
1
2
3
4
5
6
7
8
9
Done joining thread
Detected thread joining with id: 10
We are a bit limited on what we can do, but we are able to inject code and access the thread reference. Hopefully this will be enough for you, if not we can still try to discuss that a bit more.
With all this ideas in mind we create the following injector:
ClassPool classPool = ClassPool.getDefault();
CtClass guineaPigCtClass = classPool.get(GuineaPig.class.getName());
guineaPigCtClass.instrument(new ExprEditor() {
@Override
public void edit(MethodCall m) throws CannotCompileException {
CtMethod method = null;
try {
method = m.getMethod();
} catch (NotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String classname = method.getDeclaringClass().getName();
String methodName = method.getName();
if (classname.equals(Thread.class.getName())
&& methodName.equals("start")) {
m.replace("{ System.out.println(\"Detected thread starting with id: \" + ((Thread)$0).getId()); $proceed($$); } ");
} else if (classname.equals(Thread.class.getName())
&& methodName.equals("join")) {
m.replace("{ System.out.println(\"Detected thread joining with id: \" + ((Thread)$0).getId()); $proceed($$); } ");
}
}
});
guineaPigCtClass
.writeFile("<Your root directory with the class files>");
}
So what's happening in this small nifty piece of code? We use an ExprEdit to instrument our GuineaPig class (without doing any harm to it!) and intercept all method calls.
When we intercept a method call, we first check if the declaring class of the method is a Thread class, if that's the case it means we are invoking a method in a Thread object. We then proceed to check if it's one of the two particular methods start
and join
.
When one of those two cases happen, we use the javassist highlevel API to do a code replacement. The replacement is easy to spot in the code, the actual code provided is where it might be a bit tricky so let's split one of those lines, let's take for example the line that will detect a Thread starting:
{ System.out.println(\"Detected thread starting with id: \" + ((Thread)$0).getId()); $proceed($$); } "
You can read more about this special operators in Javassist tutorial, section 4.2 Altering a Method Body (search for MethodCall subsection, sorry there's no anchor for that sub-section)
Finally after all this kung fu we write the bytecode of our ctClass into the class folder (so it overwrites the existing GuinePig.class file) and when we execute it... voila, the output is now what we wanted :-)
Just a final warning, keep in mind that this injector is pretty simple and does not check if the class has already been injected so you can end up with multiple injections.