Java compatibility, Apache Maven and Jenkins Best Practices

Context

Java compatibility is a complex issue. While in the pre-java 9 world this topic was coming on the table only every few years, the new 6 months release cadence introduced with Java 9 will change the story.

Each major version of Java provides a mixup of changes in the language, the APIs and the tools (non exhaustive list):

  • 2018-09-25: Java 11 - New ZGC and Epsilon garbage collectors; Local-Variable Syntax for Lambda Parameters; New standard HTTP library; Enhanced KeyStore mechanisms; TLS 1.3; Java EE, CORBA, Web Start and Applets removal; JavaFX moved out to OpenJFX, …
  • 2018-03-20: Java 10 - Local-Variable Type Inference, Garbage-Collector Interface, Parallel Full GC for G1, Application Class-Data Sharing, Thread-Local Handshakes, Remove the Native-Header Generation Tool (javah), Additional Unicode Language-Tag Extensions, Heap Allocation on Alternative Memory Devices, Root Certificates, …
  • 2017-09-21: Java 9 - The Java Platform module system (Jigsaw), Linking (jlink), JShell (interactive Java REPL), Improved Javadoc, Collection factory methods, Stream API improvements, Private interface methods, HTTP/2, Multi-release JARs, …
  • 2014-03-18: Java 8 - Lambda Expressions, Pipelines and Streams, Date and Time API, Default Methods, Type Annotations, No more PermGen, TLS SNI, …
  • 2011-07-28: Java 7 - Strings in switch Statement, Type Inference for Generic Instance Creation, Multiple Exception Handling, Support for Dynamic Languages, Try with Resources, Java NIO Package, Diamond Syntax, …
  • 2006-12-11: Java 6 - Scripting Language Support, JDBC 4.0 API, Java Compiler API, Pluggable Annotations, Native PKI, Kerberos and LDAP support, ….
  • 2004-09-30: Java 5 - Generics, Enhanced for Loop, Autoboxing/Unboxing, Typesafe Enums, Varargs, Static Import, Metadata (Annotations), …

This article is explaining what are the best practices when using Apache Maven and Jenkins to handle these upgrades (more) smoothly.

Java compatibility

There are several types of potential incompatibilities related to a release of the Java platform:

  1. Source compatibility concerns translating Java source code into class files including whether or not code still compiles at all.

    Code originally written for an older JDK version should almost always compile without modification with a newer Java compiler (but there are a number of small incompatibilities like enum keyword added in Java 5).

  2. Binary compatibility is defined in The Java Language Specification as:

A change to a type is binary compatible with (equivalently, does not break binary compatibility with) pre-existing binaries if pre-existing binaries that previously linked without error will continue to link without error.

The JVM is backwards compatible, as it can run older bytecodes. You should be able to run Java code compiled with older versions on newer versions without modification (There might, however, be small incompatibilities). 

For example, an application built for Java 7 should run on a Java 8 JVM. On the other side (Trying to run a Java 8 compiled application on a Java 7 VM) it won't work and you'll get an error like `java.lang.UnsupportedClassVersionError: Unsupported major.minor version 52.0`.
  1. Behavioral compatibility includes the semantics of the code that is executed at runtime.
  2. APIs compatibility includes all changes (additions, updates, removals) in the default APIs provided by the Java Platform. Removal of a method signature may cause your application to fail to compile or fail to run with a more recent version of the JDK.
  3. Tools compatibility is about the toolset provided by the Java Platform. The JDK provides more than just the compiler javac. The list of tools is evolving and sometimes their parameters are changing.

The best option is to build and assemble your application with the same JDK version as the Java Platform version used at execution time. But we’ll see below that for various reasons that is not always possible.

Java compatibility with Apache Maven

  1. Source and Binary compatibility

    By default your build relies on the Java JDK provided by the developer and/or your CI/CD environment which isn’t necessarily the one your application is targeting at runtime.

    Apache Maven offers various features to avoid such incompatibilities:

    • Always set the -source and -target of the Java Compiler preferably using the properties maven.compiler.source and maven.compiler.target which is allowing others plugins to be automatically configured.

      <project>
        [...]
        <properties>
          <maven.compiler.source>1.8</maven.compiler.source>
          <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
        [...]
      </project>
      
    • For a strict control the maven-enforcer-plugin with the Require Java Version rule is allowing you to fail the build if the JDK doesn’t match the version you are targeting.

      <project>
        [...]
        <build>
          <plugins>
            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-enforcer-plugin</artifactId>
              <version>???</version>
              <executions>
                <execution>
                  <id>enforce-java</id>
                  <goals>
                    <goal>enforce</goal>
                  </goals>
                  <configuration>
                    <rules>
                      <requireJavaVersion>
                        <version>1.8.0</version>
                      </requireJavaVersion>
                    </rules>    
                  </configuration>
                </execution>
              </executions>
            </plugin>
          </plugins>
        </build>
        [...]
      </project>
      
    • Because your application is often using a lot of dependencies the maven-enforcer-plugin with the Enforce Bytecode Version is allowing to check that your project dependencies aren’t built for a Java version higher than the one you are targeting.

      <project>
        [...]
        <build>
          <plugins>
            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-enforcer-plugin</artifactId>
              <version>???</version>
              <dependencies>
                <dependency>
                  <groupId>org.codehaus.mojo</groupId>
                  <artifactId>extra-enforcer-rules</artifactId>
                  <version>???</version>
                </dependency>
              </dependencies>
              <executions>
                <execution>
                  <id>enforce-bytecode-version</id>
                  <goals>
                    <goal>enforce</goal>
                  </goals>
                  <configuration>
                    <rules>
                      <enforceBytecodeVersion>
                        <maxJdkVersion>1.8</maxJdkVersion>
                      </enforceBytecodeVersion>
                    </rules>
                    <fail>true</fail>
                  </configuration>
                </execution>
              </executions>
            </plugin>
          </plugins>
        </build>
        [...]
      </project>
      
  2. Behavioral compatibility

    Tests, tests, tests … Only automated tests can help you to detect changes in the behavior/sematic of the Java (or external) APIs you are using.

  3. APIs compatibility

    To reduce the risk of APIs incompatibilities, if you are developing with a more recent Java version than the targeted one, the maven-enforcer-plugin with the Animal Sniffer Enforcer Rule or the Animal Sniffer Maven Plugin should be used to check your API usages based on the targeted Java Runtime APIs signatures.

    <project>
      [...]
      <build>
        <plugins>
          <plugin>
            <artifactId>maven-enforcer-plugin</artifactId>
            <version>???</version>
            <dependencies>
                <dependency>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>animal-sniffer-enforcer-rule</artifactId>
                    <version>???</version>
                </dependency>
            </dependencies>
            <executions>
              <execution>
                <id>check-signatures</id>
                <phase>test</phase>
                <goals>
                  <goal>enforce</goal>
                </goals>
                <configuration>
                  <rules>
                    <checkSignatureRule implementation="org.codehaus.mojo.animal_sniffer.enforcer.CheckSignatureRule">
                      <dependency>
                        <groupId>org.codehaus.mojo.signature</groupId>
                        <artifactId>java18</artifactId>
                        <version>1.0</version>
                      </dependency>
                    </checkSignatureRule>
                  </rules>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
      [...]
    </project>
    
  4. Tools compatibility

    Apache Maven and its ecosystem of plugins provide an abstraction on top of JDK tools like javac, javadoc, etc. The different plugins are handling the specifics of each version automatically, but there are some cases where it’s necessary to manually tune their settings.

    For example, if your build needs to run on different JDKs you can adapt your settings per JDK version using the following:

    <project>
      [...]
      <profiles>
        <profile>
          <id>java8</id>
          <activation>
            <jdk>1.8</jdk>
          </activation>
          <build>
            <pluginManagement>
              <plugins>
                <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-javadoc-plugin</artifactId>
                  <configuration>
                    <!-- Turning off doclint in JDK 8 Javadoc -->
                    <doclint>none</doclint>
                  </configuration>
                </plugin>
              </plugins>
            </pluginManagement>
          </build>    
        </profile>
      </profiles>
      [...]
    </project>
    

    By default, Apache Maven is using the same JDK than the one used to build your project, but as we stated above:

    The best option is to always use the same version of the JDK to build and assemble your application as the Java Platform version used at execution time.

    Thus what can we do if the JDK version used by Maven isn’t the same as targeted by your application?

    For example, it could be useful if you want to use a recent Maven version to build a project that requires an older JDK.

    • Apache Maven 2.0 to 2.1.0 requires Java 1.4
    • Apache Maven 2.2.0 to 3.1.1 requires Java 1.5
    • Apache Maven 3.2.1 to 3.2.5 requires Java 1.6
    • Apache Maven 3.3.1 and newer requires Java 7

    To achieve this goal you can configure the plugins to use a specific tool like the compiler plugin but you can also adopt a more global approach using the Maven Toolchains.

    To use the toolchains you have to define on your agent a configuration file (${user.home}/.m2/toolchains.xml by default) where you reference all your JDKs:

    <toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
      <toolchain>
        <type>jdk</type>
        <provides>
          <version>1.6</version>
        </provides>
        <configuration>
          <jdkHome>/home/opt/jdk1.6</jdkHome>
        </configuration>
      </toolchain>
      <toolchain>
        <type>jdk</type>
        <provides>
          <version>1.7</version>
        </provides>
        <configuration>
          <jdkHome>/home/opt/jdk1.7</jdkHome>
        </configuration>
      </toolchain>
      <toolchain>
        <type>jdk</type>
        <provides>
          <version>1.8</version>
        </provides>
        <configuration>
          <jdkHome>/home/opt/jdk1.8</jdkHome>
        </configuration>
      </toolchain>
      [...]
    </toolchains>
    

    On the project side the maven-toolchain-plugin is configured to enforce all Java related tasks (javac, javadoc …) with a given JDK.

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      [...]
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-toolchains-plugin</artifactId>
            <version>???</version>
            <executions>
              <execution>
                <goals>
                  <goal>toolchain</goal>
                </goals>
              </execution>
            </executions>
            <configuration>
              <toolchains>
                <jdk>
                  <version>1.6</version>
                </jdk>
              </toolchains>
            </configuration>
          </plugin>
        </plugins>
      </build>
      [...]      
    </project>
    

    A full sample is available here

Java compatibility with Apache Maven and Jenkins

Let’s now use Jenkins to automate the build of you application.

Jenkins proposes different kinds of jobs to which can be used to build your Maven project.

  • Pipeline job - This is the best approach.
  • Freestyle job with a Maven build step
  • Maven job - This is the most dangerous approach.

If you are using a Pipeline job, we advise you also to use the Pipeline Maven Integration which eases the environment setup and provide advanced features like triggering across jobs based on their SNAPSHOT dependencies.

If you are using a Freestyle job or Pipeline job, you can freely configure them with any JDK and Apache Maven versions. We advise you to use the Config File Provider plugin to deploy your Maven settings and inject your credentials as server entries.

If you are using the Maven job you are facing an important constraint: The JDK used by Maven must be at least as new as the version used to compile Jenkins..
It means that for any Maven Job, any Maven release, you have this additional requirement:

  • Jenkins >= 1.520 (1.532.1 for LTS) requires Java 6 thus Maven jobs must be launched with a JDK >= 6.
  • Jenkins >= 1.612 (1.625.1 for LTS) requires Java 7 thus Maven jobs must be launched with a JDK >= 7.
  • Jenkins >= 2.54 (2.60.1 for LTS) requires Java 8 thus Maven jobs must be launched with a JDK >= 8.

If you need to build a project for an older Java version with the Maven job type on a Jenkins running a newer Java version, you will have to build with the newer Java that Jenkins is using. You will also need to rely on our prior Java compatibility advice with Apache Maven to reduce the risk of incompatibility.

Have more questions?

0 Comments

Please sign in to leave a comment.