Introduction to Maven ToolchainsJanuary 25, 2022
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 17 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
j17. But doing that every time you're seeing that "release version 17 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 17, 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 17, hence we see "release version 17 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 toolchain 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 17 --> <jdk> <version>17</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 17. 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='17' ] [ERROR] No toolchain found for type jdk [ERROR] Cannot find matching toolchain definitions for the following toolchain types: jdk [ version='17' ]
That's a clear message: Maven cannot build this project as there is no JDK toolchain with version 17 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 17 toolchain that lives on my machine, I should write:
<?xml version="1.0" encoding="UTF8"?> <toolchains> <toolchain> <type>jdk</type> <provides> <version>17</version> </provides> <configuration> <jdkHome>/Library/Java/JavaVirtualMachines/adoptopenjdk-17.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
Some ten months 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.
The problem I see with toolchains is that you need to manually configure the JDKs in ~/.m2/toolchains.xml, which creates a small portability issue.
Think of CI where you use a container image (GitLab CI), that image only has a single JDK, to make it work correctly the image needs to have all the JDKs needed installed and also have the configuration of the toolchains.xml.
So while it might be practical on a developer machine, it loses that benefit on CI, and just to enforce the JDK version, better use enforcer-plugin, and now with a recent JDK you can compile your code up to Java 7 with the –release (maven.compiler.release), the toolchains is just an old mechanism for pre-Java-8 code.
The only advantage would be that Maven wasn’t compatible with recent JDK versions and your code needs to compile on a recent JDK, eg. Maven is compatible with Java 11 but not with Java 17, and you need to compile with Java 17, but actually, this is not the case (I know that Gradle suffers when a new JDK is released and toolchains is the only way to do this).
To a certain extent, that is right, having to configure the Toolchains upfront means a bit of work configuring the CI server. In case of a containerised build, it may be more convenient to split the build, one for each JDK. But not everyone is using containerised builds, and sometimes all you have is a big Jenkins box with 4 different JDK’s installed. Which JDK to use? The Toolchain specifies that.
In a containerised build, you could even generate the toolchains.xml as an early stage in the build. No benefits lost. Usually, a project specifies only one JDK to be used in the build, so no need to install multiple JDK’s in one container image.
But make no mistake, the primary goal of Toolchains is not to enforce a particular JDK version. Indeed, there’s the Enforcer Plugin for that. Toolchains help you decouple the JDK that compiles your code from the JVM that Maven uses. In that sense, it _increases_ portability: no matter what JVM a developer uses on their machine, their team members might have a different one, and the build server yet another one. And that’s fine because Toolchains ensure that the build always uses the same, predictable JDK, no matter your local setup.
[…] Автор оригинала: Maarten Mulders […]