Search code examples
javassist

How to detect that a thread has started using javassist?


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 ?


Solution

  • Short Answer

    You can do this by creating an ExprEditor and use it to modify MethodCalls that match with start and join of thread objects.

    (very) Long answer (with code)

    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($$); } "

    1. First all the code is inside curly brackets, otherwise javassist won't accept it
    2. Then you have a System.out that references a $0. $0 is a special parameter that can be used in javassist code manipulations and represents the target object of the method call, in this case we know for sure it will be a Thread.
    3. $proceed($$) This probably is the trickiest instruction if you're not familiar with javassist since it's all javassist special sugar and no java at all. $proceed is the way you have to reference the actual method call you are processing and $$ references to the full argument list passed to the method call. In this particular case start and join both will have this list empty, nevertheless I think it's better to keep this information.

    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.