A Maven project that produces Java 17 and Java 8 JAR files

Marcio Endo
February 7, 2022

Introduction

Java library authors must choose a minimum version of the Java platform to support. The decision will be a balance between the (possible) size of programmer audience and the author's own development effort.

Put in other words, choosing a higher Java version to support may prevent a number of programmers from trying out the library. The programmers might use a lower Java version at their place of work. On the other hand, choosing a lower Java version limits the Java language features and the JDK API that library authors can use.

At times, however, it might make sense for the library authors to break out from this restriction. Meaning that it might make sense to provide to different users different implementations of the same API. One example is when a newer JDK provides a more performant API.

In this post, I will show you a Maven project that is capable of producing two distinct JAR files of the same API. In a first build pass, a Java 17 JAR file is produced. The classes in this JAR file are compiled and tested with a JDK 17. In a second separate build pass, a Java 8 JAR file is produced. The classes in this JAR file are compiled and tested with a JDK 8. Additionally, while both JAR files provide the same API, the implementation for one of the classes is different.

Leveraging newer JDK API

Java 9 introduced the Arrays.mismatch(byte[], byte[]) method. The method returns the index of the first mismatch between two byte arrays or it returns -1 if there is no mismatch. If you dig into its implementation, in the ArraySupport class you will find the following Javadocs:

The mismatch method implementation, vectorizedMismatch, leverages vector-based techniques to access and compare the contents of two arrays. (...) For a byte[] array, 8 bytes (64 bits) can be accessed and compared as a unit rather than individually, which increases the performance when the method is compiled by the HotSpot VM.

Suppose that you, as a library author, needs to compare arrays of bytes between themselves. Suppose also that you have decided that the minimum Java version supported by your library is Java 8. Your library, being a Java 8 project, cannot use the mentioned method as it is not available to you.

Additionally, any programmer, while using your library, will also loose the performance improvement even if they run it on a Java 9+ JVM.

For the latter to not happen, you could provide to different users different implementations of your library depending on the Java platform version your users will be running.

A quick note on JEP 238

From the library author side of the problem, the preferred solution would be JEP 238: Multi-Release JAR Files.

It has many obvious advantages. The most obvious one, in my opinion, it that it is officially and natively supported by the JDK.

So if you can, I would recommend using it.

This post, however, is not about JEP 238.

A façade to Arrays.mismatch

To illustrate this post, I have written a simple Java library. It is a façade to the Arrays.mismatch method mentioned earlier in this post. In a program, you would use it like so:

byte[] a = // first array
byte[] b = // second array
int result = ByteArrays.mismatch(a, b);

Depending on which JAR file you consume, you get different implementations:

So users invoke the same API, the façade, regardless of the Java version they are running on. However, users on Java 17 (and consuming the Java 17 version of the library) should experience an improved performance as it uses the JDK provided optimized solution.

Build requirements

To build our example project, the following is required:

Apart from these required tools, I highly recommend using a working directory for running the example project. The reason being that the build process will download and create many files. So it will be easier to clean up after we finish.

Please note that any non-absolute path name given from this point forward will be relative to this path.

Additionally, the build scripts require that JDKs and Maven be accessible directly from the working directory like so:

$ pwd
/home/mendo/objectos/jarfiles

$ ls -l
total 0
lrwxrwxrwx 1 mendo mendo 20 fev  6 21:44 jdk17 -> /opt/jdk/jdk-17.0.2/
lrwxrwxrwx 1 mendo mendo 22 fev  7 08:07 jdk8 -> /opt/jdk/jdk8u322-b06/
lrwxrwxrwx 1 mendo mendo 24 fev  7 08:07 mvn3 -> /opt/apache-maven-3.8.4/

$ jdk17/bin/java -version
openjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-86)
OpenJDK 64-Bit Server VM (build 17.0.2+8-86, mixed mode, sharing)

$ jdk8/bin/java -version
openjdk version "1.8.0_322"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_322-b06)
OpenJDK 64-Bit Server VM (Temurin)(build 25.322-b06, mixed mode)

$ JAVA_HOME=jdk17/ mvn3/bin/mvn --version
Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)
Maven home: /home/mendo/objectos/jarfiles/mvn3
Java version: 17.0.2, vendor: Oracle Corporation, runtime: /opt/jdk/jdk-17.0.2
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.15.11-gentoo", arch: "amd64", family: "unix"

This should be enough to build the example in this post.

Downloading the example

You can browse the source code for this example here.

Let's get all of the required files for our build:

$ wget --cut-dirs=2 \
     --no-host-directories \
     --no-parent \
     --recursive \
     --reject "src.html*" \
     https://www.objectos.com.br/blog/maven-project-that-produces-java17-and-java8-jar-files/src.html

And let's verify that we got all of the required files:

$ find bin/ library/ -type f | sort
bin/mvn17
bin/mvn8
library/pom.xml
library/src/main/java/library/ByteArrays.java
library/src/main/java/library/Mismatch.java
library/src/main/java17/library/MismatchJava17.java
library/src/main/java17/module-info.java
library/src/main/java8/library/MismatchJava8.java
library/src/test/java/library/ByteArraysTest.java
library/toolchains.xml

Let's ensure there are no rm -rf / or similar in the downloaded scripts:

$ cat bin/* | grep --invert-match ^# | grep .
mvn3/bin/mvn \
  --activate-profiles java17 \
  --define maven.repo.local=m2/repo-java17 \
  --file library/pom.xml \
  --no-transfer-progress \
  --toolchains library/toolchains.xml \
  "$@"
mvn3/bin/mvn \
  --activate-profiles java8 \
  --define maven.repo.local=m2/repo-java8 \
  --file library/pom.xml \
  --no-transfer-progress \
  --toolchains library/toolchains.xml \
  "$@"

Seems all right. Finally, let's make the scripts executable:

$ chmod +x bin/*

A quick note on ByteArrays and MismatchSingleton

The 'entry point' of our library is the ByteArrays class. If you look at the source code of the ByteArrays class, you will notice it references a non-existent MismatchSingleton type:

package library;

public final class ByteArrays {
  private ByteArrays() {}

  public static int mismatch(byte[] a, byte[] b) {
    return MismatchSingleton.INSTANCE.mismatch(a, b);
  }
}

The source code for the MismatchSingleton will be generated at compilation time by an annotation processor from Objectos Latest.

Building the Java 17 JAR file

Now that we have our build scripts setup, we can proceed and build our project.

Let's start with the Java 17 version.

Before we do, please note that:

OK, let's build by invoking the verify phase.

$ bin/mvn17 verify
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< br.com.objectos.www:library >---------------------
[INFO] Building library 1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------

The example project is a simple JAR Maven project having the following attributes:

It takes some time as Maven has to download all of artifacts for all of the plugins.

[INFO] --- maven-toolchains-plugin:3.0.0:toolchain (toolchains-jdk) @ library ---
[INFO] Required toolchain: jdk [ version='17' ]
[INFO] Found matching toolchain for type jdk: JDK[jdk17/]

The first plugin in the execution is the Maven Toolchains Plugin. It says it requires a JDK toolchain with the version='17' condition. In the following line it finds the JDK 17 toolchain in the working directory.

[INFO] --- build-helper-maven-plugin:3.3.0:add-source (add-source) @ library ---
[INFO] Source directory: /home/mendo/objectos-jarfiles/library/src/main/java17 added.
[INFO] Source directory: /home/mendo/objectos-jarfiles/library/src/main/java8 added.

The next plugin in the execution is the Build Helper Maven Plugin. We can see that it adds two source directories to the build process:

[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ library ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] skip non existing resourceDirectory /home/mendo/objectos/jarfiles/library/src/main/resources

Then the Maven Resources Plugin does its default execution.

[INFO] --- maven-compiler-plugin:3.9.0:compile (default-compile) @ library ---
[INFO] Toolchain in maven-compiler-plugin: JDK[jdk17/]
[INFO] Compiling 5 source files to /home/mendo/objectos-jarfiles/library/target-java17/classes

Now the Maven Compiler Plugin. We can see that the plugin:

[INFO] --- build-helper-maven-plugin:3.3.0:add-test-source (add-test-source) @ library ---
[INFO] Test Source directory: /home/mendo/objectos/jarfiles/library/src/test/java17 added.
[INFO] Test Source directory: /home/mendo/objectos/jarfiles/library/src/test/java8 added.
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ library ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] skip non existing resourceDirectory /home/mendo/objectos/jarfiles/library/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.9.0:testCompile (default-testCompile) @ library ---
[INFO] Toolchain in maven-compiler-plugin: JDK[jdk17/]
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /home/mendo/objectos/jarfiles/library/target-java17/test-classes

Next the Build Helper Maven Plugin, the Maven Resources Plugin and the Maven Compiler Plugin do the same steps as before but for the tests.

[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ library ---
[INFO] Toolchain in maven-surefire-plugin: JDK[jdk17/]

Later in the process, we can see that the Maven Surefire Plugin, like the Maven Compiler Plugin before, is also using the JDK 17 toolchain for running the tests.

[INFO] Results:
[INFO]
[INFO] Tests run: 64, Failures: 0, Errors: 0, Skipped: 0

OK, all tests green.

[INFO] --- maven-jar-plugin:3.2.2:jar (default-jar) @ library ---
[INFO] Building jar: /home/mendo/objectos-jarfiles/library/target-java17/library-1-SNAPSHOT.jar

Finally, we can see that the Maven JAR Plugin writes the JAR file to the target-java17 directory.

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:09 min
[INFO] Finished at: 2022-02-07T08:30:18-03:00
[INFO] ------------------------------------------------------------------------

OK, it seems that everything was compiled and tested with the JDK 17 in our working directory.

Let's confirm by checking one of the compiled classes:

$ jdk17/bin/javap \
  -verbose \
  library/target-java17/classes/library/ByteArrays.class \
  | grep major
major version: 61

Major version is 61 which, according to this table, is the correct value for Java 17.

Let's see the generated source code:

$ cat library/target-java17/generated-sources/annotations/library/*
package library;

import br.com.objectos.latest.Generated;

@Generated("br.com.objectos.latest.processor.SingletonProcessor")
final class MismatchSingleton {
  static final library.MismatchJava17 INSTANCE = MismatchJava17.INSTANCE;

  private MismatchSingleton() {}
}

The MismatchSingleton is very simple and references the MismatchJava17 type.

Finally let's see the classes on the Maven generated JAR file:

$ jdk17/bin/jar \
  --file library/target-java17/library-1-SNAPSHOT.jar \
  --list \
  --verbose \
  | grep \.class$
 836 Sun Feb 06 11:02:44 BRT 2022 library/MismatchJava8.class
 437 Sun Feb 06 11:02:44 BRT 2022 library/MismatchSingleton.class
 497 Sun Feb 06 11:02:44 BRT 2022 library/ByteArrays.class
 525 Sun Feb 06 11:02:44 BRT 2022 library/MismatchJava17.class
 294 Sun Feb 06 11:02:44 BRT 2022 library/Mismatch.class
 246 Sun Feb 06 11:03:02 BRT 2022 module-info.class

We can see it contains the compiled class of the generated MismatchSingleton. It also contains a compiled module-info.class class file.

Building the Java 8 JAR file

Next, let's build the Java 8 version by invoking the verify phase.

$ bin/mvn8 verify
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< br.com.objectos.www:library >---------------------
[INFO] Building library 1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------

OK, the same coordinates as the Java 17 version.

[INFO] --- maven-toolchains-plugin:3.0.0:toolchain (toolchains-jdk) @ library ---
[INFO] Required toolchain: jdk [ version='1.8' ]
[INFO] Found matching toolchain for type jdk: JDK[jdk8/]

This time, we see that the Maven Toolchains Plugin requires a JDK toolchain with the version='1.8' condition. It finds and selects the JDK 8 in the working directory for the build.

[INFO] --- build-helper-maven-plugin:3.3.0:add-source (add-source) @ library ---
[INFO] Source directory: /home/mendo/objectos-jarfiles/library/src/main/java8 added.

The Build Helper Maven Plugin only adds a single source set to the build: src/main/java8.

[INFO] --- maven-compiler-plugin:3.9.0:compile (default-compile) @ library ---
[INFO] Toolchain in maven-compiler-plugin: JDK[jdk8/]
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to /home/mendo/objectos-jarfiles/library/target-java8/classes

The Maven Compiler Plugin uses JDK 8 and compiles only 3 source files. This is different from the 5 source files compiled in the Java 17 version.

[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ library ---
[INFO] Toolchain in maven-surefire-plugin: JDK[jdk8/]

The Maven Surefire Plugin uses JDK 8.

[INFO] Results:
[INFO]
[INFO] Tests run: 64, Failures: 0, Errors: 0, Skipped: 0

Like the Java 17 version, all tests are green.

The important thing to keep in mind here is that the source code of the tests in this Java 8 build pass is exactly the same as the source code of the tests in the Java 17 build pass.

[INFO] --- maven-jar-plugin:3.2.2:jar (default-jar) @ library ---
[INFO] Building jar: /home/mendo/objectos-jarfiles/library/target-java8/library-1-SNAPSHOT.jar

And the Maven JAR Plugin writes the JAR file to the target-java8 directory:

Like we did in the Java 17 section, let's check the ByteArrays compiled class file:

$ jdk17/bin/javap \
  -verbose \
  library/target-java8/classes/library/ByteArrays.class \
  | grep major
major version: 52

OK, major version is 52, the correct value for Java 8.

Let's see the generated source code:

$ cat library/target-java8/generated-sources/annotations/library/*
package library;

import br.com.objectos.latest.Generated;

@Generated("br.com.objectos.latest.processor.SingletonProcessor")
final class MismatchSingleton {
  static final library.MismatchJava8 INSTANCE = MismatchJava8.INSTANCE;

  private MismatchSingleton() {}
}

The MismatchSingleton source code is almost identical to the Java 17 version, except it references a MismatchJava8 type.

Finally let's see the classes on the Maven generated JAR file:

$ jdk17/bin/jar \
  --file library/target-java8/library-1-SNAPSHOT.jar \
  --list \
  --verbose \
  | grep \.class$
 836 Sun Feb 06 11:46:30 BRT 2022 library/MismatchJava8.class
 435 Sun Feb 06 11:46:30 BRT 2022 library/MismatchSingleton.class
 495 Sun Feb 06 11:46:30 BRT 2022 library/ByteArrays.class
 294 Sun Feb 06 11:46:30 BRT 2022 library/Mismatch.class

Like the Java 17 version, it contains the compiled class of the MismatchSingleton type. Unlike the Java 17 version, it does not include the module-info.class file.

Conclusion

In this post I showed you a working Maven project capable of producing a Java 17 and a Java 8 JAR files. The Java 17 JAR file is built and tested with JDK 17. The Java 8 JAR file is built and tested with JDK 8.

They both provide the same API. We verify this fact by running the same test suite for both versions of the library.

They provide different implementations of the same API. We verify this fact by inspecting the generated source file of the MismatchSingleton class.

Next steps

In a number of upcoming posts I will explain how to incrementally change a vanilla Java 17 Maven project so it reaches the final form shown in this post.

This way I hope to explain in details how the Maven project shown in this post really works.

Thank you for reading and until the next post.