Monday, February 2, 2009

How to Tell If Your Code is Still Compatible...

... with jdk 1.4, Spring 2.0 or any other version of a framework or a library, after your last code modification? Sounds familiar? I found myself in that situation not so long ago. I was reworking some code with Spring 2.5 specific method calls to restore compatibility with Spring 2.0 and Jdk 1.4. After wading through the java docs and manually checking suspicious calls for a while, I started to fancy a tool that does the job for me; something that answers precisely questions as: what and where are code parts that reference classes or methods introduced in Java 6 and were not present in Java 1.4, for instance.

I thought ASM would be appropriate for the job even though I didn't have extensive prior experience with. Fortunately, ASM has a nice design based on the visitor pattern. Simply said you implement a visitor that responds to events fired by ASM as it sweeps through a class file. Think of something a la SAX applied to a class file if you wish. ASM also provides a tree based model although I didn't need to use it here.

The program I wrote analyzed and compared class files in 2 jars representing 2 different versions of the same library or framework. The differences between those in terms of new classes or new methods in existing classes were calculated and used to identify method calls specific to the newer version, from within the code I was working on.
That worked nicely and above all, helped me get the job done. There were a couple of limitations though such as method parameters being printed in the internal bytecode format, and hard-coded configuration.
That's the story, but we are not done yet.

Groovy and Gant, the Icing on the Cake

When it comes to Groovy I'm a bit of a self-indulgent, I must admit. I can't help wondering how to make the grass greener with Groovy. I set myself to write a more usable tool around the functionalities described earlier. Groovy lent itself particularly well to the task as I will explain later. But first, let's see what the resulting tool, affectionately dubbed MAD for My ASM Diff, does:

  • display information about classes in a given jar
  • display differences in terms of classes between 2 jars
  • display differences in terms of methods between 2 jars
  • display references to new classes or methods from within a jar.
This last functionality is probably the most useful as it allows to detect the incompatibility issues.
Gant comes to mind naturally at this point. Basically, it could be seen as a Groovy-based rehabilitation of Ant. It sanely replaces Ant's obnoxious XML with Groovy scripts. As much as I appreciate the diversity and power of Ant's tasks, I always felt a certain repulsion towards scripting in XML. All Ant's tasks that we've come to know and love are accessible through the builder abstraction in Gant's scripts. They could be mixed and matched with regular Groovy code, which is quite helpful.
I used Gant to organise the tool into tasks, invocable and parametrisable from the command line, but also to derive benefit from Maven's Ant tasks. I used those to download automatically the artifacts to analyse and to their source code.

Using MAD

First you need to install Groovy and Gant. The instructions are simple and can be found on their respective web sites. For a good introductory article on Gant check out this one on Java World.

The entry point is build.gant. The different operations support either jar files in the file system or more conveniently, a reference to a Maven artifact, which will be downloaded and installed in the local Maven repository. Let's see how to invoke the info command, which simply prints the name of the classes and the methods they enclose from a jar. For example, let's examine the jar of the Spring framework:

gant -Dfile=/some/where/on/the/file/system/spring.jar info
You will see something similar to the following scrolling on the console:

...
org.springframework.aop.AopInvocationException {
 <constructor>(java.lang.String)
 <constructor>(java.lang.String, java.lang.Throwable)
}
org.springframework.aop.BeforeAdvice {
}
org.springframework.aop.ClassFilter {
 matches(java.lang.Class):boolean
 <static initializer>
}
org.springframework.aop.DynamicIntroductionAdvice {
 implementsInterface(java.lang.Class):boolean
}
...
We can just point to a Maven artifact instead of a jar file by defining the artifact property (groupId:artifactId:version). The artifact will be downloaded and installed if it doesn't exist in the local repository. We will also use the output option to redirect the output to a specific file.

gant -Dartifact=org.springframework:spring:2.5.6 -Doutput=spring info
The classDiff and methodDiff commands do what you expect them to. They are useful to find out changes in terms on newly added classes or methods added to existing classes. The first jar is specified by artifact1 or file1 and the second by artifact2 or file2 properties, depending on whether you want to use Maven or not. Note that the order of the jars matters: those specified with properties suffixed with 2 will be subtracted from those suffixed with 1.

For example, let's find out what new classes were introduced in Spring 2.5 compared to Spring 2.0:

gant -Dartifact1=org.springframework:spring:2.5.6 -Dartifact2=org.springframework:spring:2.0.8 classDiff
Type the following line to display new methods:

gant -Dartifact1=org.springframework:spring:2.5.6 -Dartifact2=org.springframework:spring:2.0.8 methodDiff
Finally, the methodCalls task finds the method calls specific to the newer version for a target jar. In my case, I had to find calls to Spring 2.5 methods from Spring-WS modules:

gant -Dartifact1=org.springframework:spring:2.5.6 -Dartifact2=org.springframework:spring:2.0.8 -Dartifact=org.springframework.ws:spring-xml:1.5.5 -DdisplaySource methodCalls
The displaySource option triggers the download of the sources of the target jar and displays the lines around the incriminated call. In the previous example the output pinpoints to a call in the SaxUtils class, line 52:

org.springframework.xml.sax.SaxUtils{
  getSystemId(org.springframework.core.io.Resource):java.lang.String
    ==> org.springframework.core.io.Resource.getURI():java.net.URI [52]
  [
        org/springframework/xml/sax/SaxUtils.java:
            [51]:     try {
            [52]:         return resource.getURI().toString();
            [53]:     }
  ]
}
That's it. Note that the checks are shallow; only first level calls are checked.

Afterthoughts

I've been using Groovy for a while to perform all kinds of stunts but this was my first project fully written in Groovy. Here are some afterthoughts about using Groovy and Gant. Obviously, these are not necessarily true for other contexts or tastes.

  • Groovy is great for prototyping. I didn't need to write a whole lot of code or to set up a complex project to get started. Just create a script and start experimenting!
  • The nicest thing about Gant scripts is that they are just Groovy scripts! Good coding practices apply and common code could be refactored into methods for example.
  • Writing utility classes as scripts make things a bit more readable. Just create a script (no class declaration) and declare the utility methods with a static modifier.
  • Nested methods are (almost) back! I used method-scoped closures as a substitute to nested methods actually. Quite practical to organise bulky code in some places.
  • Iteration methods such as find and findAll are nice and useful but they are no magic wands. Don't be blinded by the ease of use and consider whether their sequential performance (O(n)) is appropriate for the job at hand. At one location inside a loop, replacing a find with a sort (outside the loop) and a binary search boosted the performance drastically. More generally, it's quite useful to take a look at DefaultGroovyMethods to see what's going on under the hood.
  • Regexp support is really awesome in Groovy, specially regexp matching in switch statements.
  • Implementing an interface with a closure or a map is quite handy in the prototyping phase. I wonder if there is an easy way to create an adpater for an arbitrary interface, meaning that calls to undefined methods return a predefined values?
  • Groovy could use better tooling support. Hopefully, this could only get better!

Download

  • Download MAD.
  • Download Gant. Gant ships with Maven Ant tasks and ASM.