It's common for a Java project to compile with later versions of JDK than it minimally requires. For example, when Hudson runs on Java6 it takes advantages of those features, but it can also run on Java5 without those advanced features.

The technique to do this is well understood. Here's one such code fragment taken from Hudson:

try {
    for (ThreadInfo ti : Functions.getThreadInfos())
        r.put(ti.getThreadName(),Functions.dumpThreadInfo(ti));
} catch (LinkageError _) {
    // not in JDK6. fall back to JDK5
    ...
}

This is desirable, since you can take advantages of the latest JavaSE features without forcing users to upgrade.

The problem is that you now need to compile with JDK6, so when other parts of code accidentally depends on new additions in Java6 and breaks the minimum Java5 requirement, your build process won't complain.  If you are lucky, your tests will catch it, but no test attain 100% of code coverage, so there's still a good chance that such a problem slips into the production code. (For example, I've been bitten a few times by using IOException(String,Throwable) constructor that was added in 1.6, since I typically use it only in handling errors that tend not to be tested well.)

Hudson had a fair share of those incidents, and it just happened one too many times for me.

 

So I decided to bite the bullet and write a tool for it.

The idea of the tool is simple. I first run a "signature builder" with JDK5, capturing all the method and field signatures from a JRE.

I then wrote a separate tool called "signature checker", which uses this signature file and inspect your classes. If your classes depend on things that don't exist in the signature list, you get an error message. This tool is packaged up as a Maven plugin, so to use this, you just add the following snippet inside your <build> element in pom.xml:


<plugin>
&#160; 
&#160; <groupId>org.jvnet</groupId>
&#160; <artifactId>animal-sniffer</artifactId>
&#160; <version>1.2</version>
&#160; <executions>
&#160;&#160;&#160; <execution>
&#160;&#160;&#160;&#160;&#160; <goals>
&#160;&#160;&#160;&#160;&#160;&#160;&#160; <goal>check</goal>
&#160;&#160;&#160;&#160;&#160; </goals>
&#160;&#160;&#160;&#160;&#160; <configuration>
&#160;&#160;&#160;&#160;&#160;&#160;&#160; <signature>
&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <groupId>org.jvnet.animal-sniffer</groupId>
&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <artifactId>java1.5</artifactId>
&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <version>1.0</version>
&#160;&#160;&#160;&#160;&#160;&#160;&#160; </signature>
&#160;&#160;&#160;&#160;&#160; </configuration>
&#160;&#160;&#160; </execution>
&#160; </executions>
</plugin>

The nested <signature> element specifies the signature list to use. In addition to java1.5, I've got java1.3, java1.4, and java1.6 available.

If you don't want to do this for every Maven build, you can instead have the following snippet:


<plugin>
&#160; 
&#160; <groupId>org.jvnet</groupId>
&#160; <artifactId>animal-sniffer</artifactId>
&#160; <version>1.2</version>
&#160; <configuration>
&#160;&#160;&#160; <signature>
&#160;&#160;&#160;&#160;&#160; <groupId>org.jvnet.animal-sniffer</groupId>
&#160;&#160;&#160;&#160;&#160; <artifactId>java1.5</artifactId>
&#160;&#160;&#160;&#160;&#160; <version>1.0</version>
&#160;&#160;&#160; </signature>
&#160; </configuration>
</plugin>

And then you can run mvn compile animal-sniffer:check to check the dependency, or you can further add the following POM snippet so that the check is performed automatically during a release:


<plugin>
&#160; <artifactId>maven-release-plugin</artifactId>
&#160; <configuration>
&#160;&#160;&#160; <goals>install animal-sniffer:check deploy</goals>
&#160; </configuration>
</plugin>

The tool uses ASM and statically analyze the code, so it doesn't miss any reference, unlike test based approach.

Now, in the places where you knowingly use features that go beyond the minimum requirement, you put the @IgnoreJRERequirement annotation on a method. This is a signal from you to the checker that you're aware of the dependency there and you know what you are doing.

For this code to compile, you need to add animal-sniffer.jar to the dependency list. This annotation is configured as @Retention(CLASS), so you don't need this jar to be at runtime. To tell Maven not to put it in the runtime, this fragment includes <optional>true</optional>:


<dependency>
&#160; 
&#160; <groupId>org.jvnet</groupId>
&#160; <artifactId>animal-sniffer-annotation</artifactId>
&#160; <version>1.0</version>
&#160; <optional>true</optional>
</dependency>

There are certain edge cases that this tool doesn't handle correctly (like the case when a visibility of a method changes from 'protected' to 'public' between Java5 to Java6 ? not that I know such a case exists), but I think it runs pretty well, at least on Hudson, and the added peace of mind is priceless.

As usual, I'm always looking for more people to work on any of my projects, so if you are interested, please send me an e-mail, and you'll be a committer right away.

Finally, the reason the tool is called animal-sniffer is because JavaSE code names are traditionally named after animals, like Mantis, Tiger, Mustang, Dolphin, and so on.