So here was the problem. A customer was using a 3rd-party library and identified a bug in the library software. The problem popped up in one particular method of the library. This method wrote data via a socket on a specific port to a remote computer. On occasion, for some reason, the socket was closed and so the data could not be written. The customer and the library vendor went back and forth pointing fingers at each other claiming the problem was either in the library or in the application that used the library.
I inspected the questionable method by decompiling its code. See my earlier article on Java decompilation. It turned out the method was very small and simple:
public void sendData(String host, int port, byte[] data) {
InetAddress address = InetAddress.getByName(host); DatagramPacket packet = new DatagramPacket(data, 0, data.length, address, port); datagramSocket.send(packet); }
The input to the method was the host and port that was to receive the data along with a byte array of the actual data to be sent. The method converted the host to an InetAddress object which was needed to create the datagram packet which was a wrapper around the recipient host and data. The last line was a call to send() which actually sent the data to the remote computer.
On occasion, something was wrong with one of the arguments and for some reason, datagramSocket had been closed and so the call to send() failed. And we had no way of knowing the host or port or data when this situation occurred. (Note that there are really a number of ways to address and debug this problem. I have created a simplified problem and solution to demonstrate the methodology I am presenting.)
At this point I want to introduce the reader to a very powerful Java toolkit call Javassist. Its documentation states:
Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If the users use the source-level API, they can edit a class file without knowledge of the specifications of the Java bytecode. The whole API is designed with only the vocabulary of the Java language. You can even specify inserted bytecode in the form of source text; Javassist compiles it on the fly. On the other hand, the bytecode-level API allows the users to directly edit a class file as other editors.
We are going to use Javassist to modify sendData() by adding a System.out.println() at the front of the method. This will be called whenever the host name changes (this additional logic is added only to show the power of Javassist). In reality, anything within the method can be altered. In our example we will replace the entire method contents with our content. Here is what we want the method to look like when modified:
public String savedHost = null; public void sendData(String host, int port, byte[] data) {
if (savedHost == null || !savedHost.equals(host)) { System.out.println("host=" + host + "; port=" + port); savedHost = host; } InetAddress address = InetAddress.getByName(host); DatagramPacket packet = new DatagramPacket(data, 0, data.length, address, port); datagramSocket.send(packet); }
So how do we use Javassist to “hack” this method and add our code? The first thing we must do is access the class itself. This is done via the following code snippet:
CtClass ctClass =
ClassPool.getDefault().get("com.dataform.MyClass");
The next step is to create our instance variable “savedHost” and associate it with our class:
CtField savedHost = CtField.make("public String savedHost;", ctClass);
Next we want to replace the existing sendData() method with our updated version of the method. There are actually a number of ways to do this. The Javassist API has a rich set of methods for inserting code into an existing method (or even creating new methods). Here I will show how to replace the existing method code with our replacement code:
CtMethod method = ctClass.getDeclaredMethod("sendData");
String newMethod =
"{" +
" if (savedHost == null || !savedHost.equals(host)) {" +
" System.out.println(\"host=\" + $1 + \"; port=\" + $2);" +
" savedHost = $1;" +
" }" +
" java.net.InetAddress address = java.net.InetAddress.getByName($1);" +
" java.net.DatagramPacket packet = new java.net.DatagramPacket($3, $3.length, address, $2);" +
" datagramSocket.send(packet);" +
"}";
method.setBody(newMethod);
Looking at the code snippet above there are a number of items of interest. First note that the arguments to the method are not referenced by name, rather by number: first argument is $1, second argument is $2, etc. Second, non-local classes, such as InetAddress were fully qualified (i.e., java.net.InetAddress). Javaassist has methods for inserting import statements but I did not use those. The last item of note is that the beginning and end of the method body have to be braces ({}). Note the last line above. The method, which is defined by the string “newMethod” is set as the replacement body of the method.
Our modified class needs to be saved as the original class is part of a library jar file. To save the class, add the following line to the code:
ctClass.writeFile(".");
In order to execute our modified class instead of the original class in the library, we need to add the following Java VM argument:
-Xbootclasspath/p:.
And that is all there is to it. You can download the Javassist library by going to http://www.javassist.org. The downloaded zip file includes a directory with three tutorial html files. All the many options are explained and described in these files.
Happy hacking!
You must be logged in to post a comment.