The Jetty Project has been trying to to build Jetty using JDK 9 for some time now.
We still have a number of things to fix (a few test cases and integration with ASM for class scanning), but overall we have reached a point where we can build Jetty with JDK 9 and will start using JDK 9 to build Jetty releases.
This process started with our first attempts using older JDK 9 releases, and the results were ugly. Until recently the Maven Plugins required to build Jetty were not updated to work with JDK 9, so trying to build Jetty with JDK 9 was not possible. After most of the Maven Plugins released versions compatible with JDK 9, fiddling with MAVEN_OPTS
and adding a number of --add-opens
we had our first success, but it was not pretty.
Fast forward to the JDK 9 release candidate (JDK 9+181) and we could build Jetty without too much effort, mostly thanks to the latest changes applied to the JDK module system. Below is a report of what we had to do, which will hopefully be of use to others.
The Jetty Project is a multi-module Maven project where we want to maintain JDK 8 compatibility, but we also want to build 2 modules that are JDK 9-specific (the client and server JDK 9 ALPN implementations).
First and foremost, you’ll want to use the latest versions of the Maven Plugins, especially the maven-compiler-plugin
(as of today at 3.6.2) and the maven-javadoc-plugin
(as of today at 3.0.0-M1). Make sure you have them all declared in a <pluginManagement>
section, so that you specify their version only there (and only once).
It’s fairly easy to build a module only when building with JDK 9:
<project ...> ... <profiles> <profile> <id>jdk9</id> <activation> <jdk>[1.9,)</jdk> </activation> <modules> <module>jdk9-specific-module</module> </modules> </profile> </profiles> </project
Next, we want to target JDK 8 for all other modules. This is typically done by configuring the maven-compiler-plugin
, specifying <target>1.8</target>
.
It turns out that this is not enough though, because while the JDK 9 compiler will produce class files compatible with JDK 8, it will compile against a JDK 9 runtime with the risk that a JDK 9 only class or method was used by mistake in the source code. Even if you are careful and you don’t use JDK 9 classes or methods in your source code, you can still hit binary incompatibilities that exist between JDK 8 and JDK 9.
Take, for example, the class java.nio.ByteBuffer
.
In JDK 8 it inherits method limit(int)
from the parent class, java.nio.Buffer
, and the exact signature is:
public final Buffer limit(int newLimit)
In JDK 9 this has changed; method limit(int)
is now overridden in java.nio.ByteBuffer
to covariantly return java.nio.ByteBuffer
, so the signature is now:
public ByteBuffer limit(int newLimit)
The difference in return type causes a binary incompatibility, so that if you compile a class that uses ByteBuffer.limit(int)
with JDK 9 and try to run with JDK 8 it fails with:
java.lang.NoSuchMethodError: java.nio.ByteBuffer.limit(I)Ljava/nio/ByteBuffer;
Indeed, JDK 8 does not have that method with that specific signature.
Luckily, the JDK 9 compiler has a new switch, --release
, that allows to easily target previous JDKs (as specified by JEP 247).
JDK 9 comes with a file ($JDK_HOME/lib/ct.sym
) that is a zipped file (you can view its content in a normal file manager) containing directories for the supported target platforms (JDK 6, 7, 8 and 9), and each directory contains all the symbols for that specific JDK platform.
The latest maven-compiler-plugin
supports this new compiler switch, so compiling correctly is now just a matter of simple configuration:
<project ...> <pluginManagement> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </pluginManagement> <profiles> <profile> <id>jdk9</id> <activation> <jdk>[1.9,)</jdk> </activation> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <release>8</release> </configuration> </plugin> </plugins> </build> </profile> </profiles> </project>
In the <pluginManagement>
section, we configure the maven-compiler-plugin
with source
and target
to be JDK 8. This section will be taken into account when compiling using JDK 8.
In the profile, activated only when compiling using JDK 9, we configure the maven-compiler-plugin
with release
to be JDK 8, so that the JDK 9 compiler generates the right class files using the symbols for JDK 8 contained in ct.sym
.
Finally, we uncovered a strange issue that we think is a Javadoc bug, apparently also hit by the Lucene Project (see https://github.com/eclipse/jetty.project/issues/1741). For us the workaround was simple, just converting an anonymous inner class into a named inner class, but your mileage may vary.
Hopefully this blog will help you build your projects with Maven and JDK 9.
2 Comments
Alan · 29/08/2017 at 10:47
Retrofitting the methods in the buffer classes to use covariant return types was a binary compatible change. The issue you ran into is that javac was being invoked with -source/-target but without the -bootclasspath option. You’ve worked around this by using the new –release option which ensures that you compile against the right version of the classes.
The blog mentions needing –add-opens in early builds. Is there any more information on this? Have bugs been submitted to the offending components that that this issue doesn’t come back once access to JDK internals are denied?
simon · 03/09/2017 at 14:10
Alan, thanks for your comments.
Regarding the need for
--add-opens
, this was needed by various Maven Plugins that were using reflection on JDK classes, see for example this issue.As far as I know, most Maven Plugins have been updated to work with JDK 9, but I’m not sure whether this has been achieved by removing nasty reflection tricks or by leveraging the now-always-on kill switch.