Recently, I came across deprecation warnings in the GMavenPlus-Maven-Plugin (gplus-maven-plugin). Here’s how I solved it by putting a Java 8-bytecode file (class file version 52.0) into META-INF/versions/java21
, which is better known as a Multi-Release JAR.
JDK-APIs to be removed
One of the APIs which are deprecated for removal is the infamous SecurityManager. As you can tell from the documentation, it is not only deprecated; instead, it is deprecated for removal. This means, code relying on these classes are going to break at some point in the future. While I expect them to work for quite a bit longer (JDK 30 or so? Who knows?), many applications do either not really need then or the functionality can be replaced somehow.
The Problem: Imports
The hardest problem to solve: You cannot use imports starting with Java 21 if you want to avoid all warnings. Now, you could just work with reflection, which is cumbersome. Another idea would be to move the code into its own class. But the moment you begin building with a version of Java which has the SecurityManager
removed, your build will break.
Luckily, Multi-Release JARs can help us get this problem out of the way.
Maven: The intended way
First, you decide which class is to be replaced in higher JDK versions by another version of itself. In my case (the groovy plugin), I created a file SecurityManagerSetter.java
with two implementations:
The Default implementation in
src/main/java
The logging-only implementation in
src/main/java21
.
Usually, you create a new source directory (e.g. src/main/java21
) with the source file mentioned above and configure a new maven-compiler-plugin execution like so:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java21</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>21</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
However, this is ugly for a very specific reason:
You either need a JDK which spans all wanted release targets (in this case: Java 8 and Java 21)
Oryou need to use Maven toolchains to start multiple compilation runs.
In this case, both options are an overkill, because we just need to replace a single file with e Java 21 version. Currently, the plugin only needs a single JDK which can produce Java 8 bytecode, which can be anything between a JDK 8 and JDK 21 (at the time of writing this article). I would not want to change that for a single file.
Maven: The creative way
So, who said we need to have a Java 21 bytecode file (class file version 65.0) in our META-INF/versions/java21
folder? I asked Nicolai Parlog who wrote this article about Multi-Release JARs, and he confirmed my guess: The class file in the Java 21 folder can be anything up to (including) Java 21 class files.
While the Maven documentation currently says, outputDirectory
is not writable, Maven 3 does not actually enforce this restriction. For the maven-compiler-plugin v3.12.0 I created a Pull Request to remove that restriction.
With that out of the way, we can start compiling files with Java 8 into the Java 21 directory like so:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java21</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<compileSourceRoots>
<compileSourceroot>${project.basedir}/src/main/java21</compileSourceroot>
</compileSourceRoots>
<!-- cannot use multireleaseOutput, because we are creating a Java 8 class file -->
<outputDirectory>${project.build.outputDirectory}/META-INF/versions/21</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
This is a simple trick: We did not specify multiReleaseOutput
, which implicitly requires the release
parameter to be set and therefore enforces the target bytecode to be the version of the release
parameter. However, as Nicolai hinted on π (formerly Twitter), there is no such restriction enforced by the JDK:
That should work, yes.
— Nicolai Parlog πΊπ¦ποΈ (@nipafx) October 20, 2023
I like that idea. Any chance you can post a summary of what you did once you get it working?
Hello, Nicolai. ππ» This is the summary! π
Back to the XML snippet — so, what do we actually do here?
We only compile files from src/main/java21
and put them into target/classes/META-INF/versions/21
. This can be done with any JDK, so the build will still run on Java 8, 11, 17, 21β¦ you name it.
Maven trickery: Compiling for JDK 30
Remember I mentioned JDK 30 a few paragraphs into this article? This is because at the time of writing, JDK 30 was not available for a loooong time: JDK 21 just came out last month, not even by all vendors due to Oracleβs TCK situation.
That said, with this trick where we uncouple the META-INF/versions/javaNN
directory from both the release
parameter of the maven-compiler-plugin and the JDK which invoked maven (or the toolchain which invoked javac
, for that matter), we can name our output directory anything you want!
How is this helpful?
In case you got an information that Java 30 will actually drop the SecurityManager
(again, this is just a wild assumption and most probably wrong), you can now put your SecurityManager
-cleansed Java 8 class file into META-INF/versions/java30
, and it will start working the moment the first Java 30 early access build arrives! Isnβt this neat? π
This also enables using a single JDK version when, by definition, multiple JDKs would have been required by the bytecode versions: JDK 30 would probably not have support for release=8
anymore.
Conclusion
In this blog post we discovered how we can avoid toolchain invocations or avoid restricting the build to higher JDK versions for simple use cases.
Did you find this helpful or do you even have similar use cases? Let Nicolai and me know on Twitter (now π)!