Ensure Class Library Compatibility in a Maven Build

Ensure Class Library Compatibility in a Maven Build

When you build jars that are supposed to work with a specific JRE version, it’s not sufficient to set the correct compiler level. Even if the compiler creates byte code for a particular language version, your code may accidentally use API that is not available in the class library of this Java version, and the compiler will not notice this.

For example, if your source code uses the Integer method toUnsignedString(int), which was added in Java 1.8 …

public static void main( String[] args ) {
  System.out.println( "Result: " + Integer.toUnsignedString( 42 ) );
}

… and your pom.xml includes the following compiler configuration …


  org.apache.maven.plugins
  maven-compiler-plugin
  3.1
  
    1.6
    1.6
  

… and if your default runtime is Java 8, the build will happily create 1.6 compatible class files that will just not run on a Java 6 VM:

$ ~/jdk/1.6.0/bin/java example.Example
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.Integer.toUnsignedString(I)Ljava/lang/String;
	at example.Example.main(Example.java:6)

Ideally, your IDE should be configured with the correct target runtime and already discover this problem. However, when you accept contributions from others, how can you ensure that no code slips in that accidentally uses newer class library features?

Solution: the Animal Sniffer

The obvious solution is to run the maven build with the target runtime environment. However, sometimes one build job includes modules with different target environments. Moreover, shouldn’t a Maven build produce reproducible results on every developer’s machine?

I’ve recently discovered a satisfactory solution to this problem: the Animal Sniffer Maven Plugin checks source code against API signatures of specific library versions (the name refers to the tradition of naming Java runtime versions after different animals). There are a number of predefined API signatures available for Java runtime environments, and you can also define your own. To check your code for Java 1.6 compatibility, you’d include this snippet in your pom.xml:


  org.codehaus.mojo
  animal-sniffer-maven-plugin
  1.9
  
    
      org.codehaus.mojo.signature
      java16
      1.0
    
  
  
    
      ensure-java-1.6-class-library
      test
      
        check
      
    
  

Now the build will discover the suspicious method and fail …

[INFO] 
[INFO] --- animal-sniffer-maven-plugin:1.9:check (ensure-java-1.6-class-library) @ example ---
[INFO] Checking unresolved references to org.codehaus.mojo.signature:java16:1.0
[ERROR] /home/ralf/tmp/animal/src/main/java/example/Example.java:6: Undefined reference: String Integer.toUnsignedString(int)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

… and you can be sure that when the build succeeds, the produced jars will actually work with the target environment.