Java evolves at a much faster pace than it used to do. But not all of the projects we work on keep up with that pace. I have projects on Java 8, 11 and 15 - and sometimes I want to play with early access builds of newer versions as well. How to make sure I can build them without having to constantly switch Java runtimes?
Switching Java versions the whole day long
Switching Java versions on the command line doesn’t have to be hard - in my case, it’s as easy as typing
But doing that everytime you’re seeing that “release version 15 not supported” is a bit tedious.
More importantly, it doesn’t solve the root cause of the issue.
So, what is the root cause, you ask?
The root cause here is that by default, the Maven Compiler Plugin will use the Java compiler that comes with the Java runtime that Maven runs in.
You can see which one that is by inspecting
$ mvn -version Apache Maven 4.0.0-alpha-1-SNAPSHOT (9e19b57c720d226b0b30992535819f700a665d14) Maven home: /usr/local/Cellar/maven-snapshot/4.0.0-alpha-1-SNAPSHOT_117/libexec Java version: 11.0.10, vendor: AdoptOpenJDK, runtime: /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home Default locale: en_GB, platform encoding: UTF-8 OS name: "mac os x", version: "10.15.7", arch: "x86_64", family: "mac"
In this example, Maven uses a Java 11 Development Kit.
But in my project, I’ve configured the Maven Compiler Plugin to set the
-release argument for the compiler to 15, by setting the
(To target Java versions below 9, you should set two properties:
The compiler from the Java 11 Development Kit obviously doesn’t know how to target Java 15, hence we see “release version 15 not supported”.
Toolchains to the rescue!
Luckily, the solution is right at our disposal. In fact, the “Compiling Sources Using A Different JDK” guide of the Maven Compiler Plugin starts with it:
The preferable way to use a different JDK is to use the toolchains mechanism.
So what exactly is a toolchain? The same guide, a few lines later, summarises:
A toolchains is a way to specify the path to the JDK to use for all of those plugins in a centralised manner, independent from the one running Maven itself.
The last part of that sentence is very important, so let me stress that once more: a toolchain is independent from the one running Maven itself.
So, how do we employ this?
First, we use the toolchain goal of the Apache Maven Toolchains Plugin to check that the toolchains requirements for a project can be satisfied using the configured toolchains:
<project> <!-- omitted for brevity --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-toolchains-plugin</artifactId> <version>3.0.0</version> <configuration> <toolchains> <!-- this project needs a JDK toolchain, version 15 --> <jdk> <version>15</version> </jdk> </toolchains> </configuration> <executions> <execution> <goals> <goal>toolchain</goal> </goals> <!-- the toolchain goal binds to the validate phase automatically --> </execution> </executions> </plugin> </plugins> </build> </project>
The above snippet says: we specify that the project needs a toolchain of type JDK with version 15. If we try to build the project again, the build still fails, but the message is different:
[INFO] --- maven-toolchains-plugin:3.0.0:toolchain (default) @ sample-project --- [INFO] Required toolchain: jdk [ version='15' ] [ERROR] No toolchain found for type jdk [ERROR] Cannot find matching toolchain definitions for the following toolchain types: jdk [ version='15' ]
That’s a clear message: Maven cannot build this project as there is no JDK toolchain with version 15 installed. Well - there is, but we didn’t tell Maven where to find it.
We can do that using the Toolchain Configuration, which lives in ~/.m2/toolchains.xml. To declare the JDK 15 toolchain that lives on my machine, I should write:
<?xml version="1.0" encoding="UTF8"?> <toolchains> <toolchain> <type>jdk</type> <provides> <version>15</version> </provides> <configuration> <jdkHome>/Library/Java/JavaVirtualMachines/adoptopenjdk-15.jdk/Contents/Home</jdkHome> </configuration> </toolchain> </toolchains>
As you can see, this file contains the full path to a Java installation. This makes the file specific to the machine where it is stored. That’s why its location is in the .m2 directory for the local user, and why the file cannot be part of the project’s source code version control. Everyone who works on the team will need their own copy of the file, adapting it as needed for the correct paths. That also includes the build servers where the project will be built!
Popularity of Toolchains
A couple of weeks ago, I asked around on Twitter to see if people know this feature, and whether they use it.
Although the response wasn’t very large, it’s interesting to have a look at the results:
- Roughly half of the people don’t know that Toolchains exist.
- Roughly a third of the people that knows Toolchains doesn’t use it.
How can this be? It seems like a powerful feature. Even the people that know Toolchains don’t always use it.
I think part of the explanation is that Maven itself is written in Java. Imagine if Maven was written in another language. In that case, you would always have to specify where to find a Java Development Kit, as Maven wouldn’t know it automatically. But now that Maven runs on the JVM, it already knows one JVM it could use. It may not be the best one for the current project, but at least it is one, and it will attempt to use it.
We already saw that the Maven Compiler Plugin understands the concept of Toolchains and knows how to use it. But that’s not the only plugin that may benefit. Indeed, many official Maven plugins understand the concept and use a toolchain when configured. This includes the Javadoc, JAR and Surefire plugins, to name a few. Even some non-official plugins work with toolchains, like the Protocol Buffer and the Keytool plugin. The full list is in the Guide to Using Toolchains.
Apart from the JDK toolchain, it is even possible to declare ones own toolchains. That is way beyond the scope of this article, but if you’re interested, the Toolchain Plugin documentation gets you started.