Injecting better logging into a binary .class using Javassist

Have you ever been strucked by a completely useless exception message somewhere from the depth of a 3rd party application or library you had to use in your code? Have you ever wanted the bloody nameless programmer to have written a truly informative and helpful error message so that you wouldn't need to spend hours trying to figure out what was the problem that would have been easily discovered if only more context information available at the moment when the exception occured was included in its error message? Have you wondered how only you could inject the necessary logging into the spot? Read on to get the answer.

Update 6/2010: You may also want to read the follow-up blog Implementing build-time bytecode instrumentation with Javassist.

Recently I was testing my extension to Saba, a monstrous J2EE learning management system, and got an exception from a Saba class saying that the expected and actual data types of a custom attribute don't match.  The problem was that I had no idea which one of the 10s of custom attributes could be the cause and I even wasn't sure which object's attributes I should check. It would be so much easier if the "nameless bloody Saba programmer" (no offense :-)) included the attribute's name and preferably also its actual & expected data types and the actual and new values. How could I insert there logging of these properties? Needless to say that I had to use Java 1.4 (no agents...) and couldn't afford more than modifying this single class file (i.e. no additional libraries etc.) because the changes I could do to the development environment where the application ran were limited.

Of course the easiest would have been to decomile the class, add the loging before the exception is thrown, recompile it and replace it on the server. But not only is decompiling illegal here, it also sometimes simply doesn't work. Fortunatelly there is another solution - JBoss Javassist is a byte code manipulation library that supports not only runtime manipulation but also post-comilation time manipulation, i.e. you can modify and save the class and use it to replace the original file. There are quite a few byte code manipulation libraries but Javassist has the great advantage that you don't need to know much about bytecode, you can simply pass a String with java statements that should be inserted before/after/... method call or into a new catch statement. There is a nice tutorial that describes it (see part 4.1, Inserting source text at the beginning/end of a method body):
addCatch() inserts a code fragment into a method body so that the code fragment is executed when the method body
throws an exception and the control returns to the caller. In the source text representing the inserted code fragment,
the exception value is referred to with the special variable $e.

For example, this program:

ClassPool pool = ClassPool.getDefault(); CtClass compiledClass = pool.get("mypackage.MyClass"); CtMethod m = compiledClass.getDeclaredMethod("myExceptionThrowingMethod"); CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ System.out.println($e); throw $e; }", etype);

translates the method body represented by m into something like this:

try { the original method body } catch (java.io.IOException e) { System.out.println(e); throw e; }

Note that the inserted code fragment must end with a throw or return statement.
You can use $e to access the exception, $0 to access "this", $1 to access the 1st parameter of the method etc. At the end you just call compiledClass.writeFile(); and use the modified mypackage/MyClass.class to replace the original class in the application.

Lovely, isn't it?

Tags: java DevOps library


Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob