Upgrading to Java 9

I've recently helped upgrade Apache Camel to Java 9 following the Quality Outreach initiative. This post is meant to help out anyone trying to do the same. Note that at the time of writing this, Java 9 hasn't been released yet so things might change but I'll try to keep this up to date.

There are some interesting talks about Java 9 which I highly recommend. It will save you a lot of time in the long run. This Java upgrade introduces a lot of changes in particular the modularization of the JDK with Project Jigsaw. I'll also recommend reading JEP 223, JEP 260 and JEP 261.

Now that you've done you're reading ;) we can start upgrading. First let's define the scope of the upgrade. As of now, the tooling support for building module-info.java files isn't there yet (although some projects are starting to surface). Therefore our goal here will be to build and run our tests with Java 9 using the new command-line flags to break encapsulation and add modules to the module path.

Before even trying to build we need to upgrade a few things.
  • Make sure you're using Maven 3.0 at the very least.
  • Upgrade some known maven plugins for Java 9 compatibility. The most common ones being the compiler(3.6.1), javadoc(2.10.4), surefire(2.19.1), war(3.0.0) and jar(3.0.2) plugins. 
  • Remove dependency on lib/tools.jar as it's no longer present. This can be done by moving this to a Maven profile with activation:
  • <activation>
    <jdk>(,9)</jdk>
    </activation>
    This will be used a lot. See the Version Range Specification for more details.
  • Use the Maven JDeps Plugin to detect usages of internal API calls and fix them if you can.

Ok now you're ready to download the latest version of Java 9 and try out your build. Fingers crossed :) By the way when working with multiple versions of Java, I've found using Jenv with the Maven extension to be very useful to easily switch between Java versions. This last section will cover the different types of errors you can encounter and how to fix them.

Troubleshooting

JEP 223

JEP 223 introduces a new versioning scheme which brakes the way we used to get the java version at runtime:

at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:289)
at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:229)
at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:415)
at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:356)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1
at org.codehaus.plexus.archiver.zip.AbstractZipArchiver.<clinit>(AbstractZipArchiver.java:116)
... 88 more
This error was seen in the plexus archiver (fixed in 3.0.3) which a lot of plugins depend upon. This was because the java version was retrieved in the following way (most java code does the same):

Integer.parseInt( System.getProperty( "java.version" ).split( "\\." )[1] )
Which poses a problem given the new versioning scheme. Here's a Java 9 compliant way of doing it:

private static int getJavaVersion(){
String javaSpecVersion = System.getProperty( "java.specification.version" );
if (javaSpecVersion.contains(".")) {//before jdk 9
return Integer.parseInt(javaSpecVersion.split("\\.")[1]);
}else {
return Integer.parseInt(javaSpecVersion);
}
}
This best way to tackle this is to search all occurences of 'java.version' in your code ahead of time and not wait to run into the issue, this will save you some build time.

JEP 261

JEP 261 introduces the module system. I'll assume you've understood the basic principles and we'll focus on troubleshooting. Here's the one you'll probably encounter first:

java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlRootElement
at org.apache.camel.tools.apt.ModelAnnotationProcessor.process(ModelAnnotationProcessor.java:49)
at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:968)
at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:884)
at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.access$2200(JavacProcessingEnvironment.java:108)
at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1204)
at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1313)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1267)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:943)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:302)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:162)
at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:55)
at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:41)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlRootElement
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:466)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:543)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:476)
... 12 more
view raw cnf-j9.error hosted with ❤ by GitHub
Following our new module system, you'll have to add the java.xml.bind module using the --add-modules flag. If you get a different missing class, check out the Java 9 overview and search you're missing class to find the module name. The logic remains the same. There are a couple ways to fix this and it's important to know the differences. First off, if this happens during build time then you can configure the maven-compiler-plugin as so:

<profile>
<id>jdk9-build</id>
<activation>
<jdk>9</jdk>
</activation>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<fork>true</fork>
<compilerArgs>
<arg>-J--add-modules</arg>
<arg>-Jjava.xml.bind</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
view raw comp.xml hosted with ❤ by GitHub
Notice the fork option is set to true and the -J to pass the flag directly to the runtime system. If this happens during tests you can configure the maven-surefire-plugin:

<profile>
<id>jdk9-build</id>
<activation>
<jdk>9</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<argLine>--add-modules java.xml.bind</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
view raw surefire.xml hosted with ❤ by GitHub
The above way of doing this is nice because these maven plugins support forking the JVM. That way you don't have to pollute the MAVEN_OPTS environment variable and users can just run the build with 'maven clean install'. However a lot of maven plugins don't support forking. This is the case for the maven-jaxb2-plugin for example. As an ultimate last resort you can also add the corresponding maven dependency to bypass the ClassNotFoundException. In our case adding the dependency:

<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.2.11</version>
</dependency>
view raw dependency.xml hosted with ❤ by GitHub
will solve our problem. I only recommend this as a last resort.

A very common thing we do in Java is reflection and proxying. Almost every framework out there does it. This will pose some problems in Java 9. For example, when using Spring proxies, one might get the following error:

testMarshallList(org.apache.camel.dataformat.bindy.fixed.marshall.simple.BindySimpleFixedLengthObjectMarshallTest) Time elapsed: 0 sec <<< ERROR!
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: java.lang.IllegalStateException: Cannot load configuration class: org.apache.camel.spring.boot.TypeConversionConfiguration
Caused by: java.lang.NoClassDefFoundError: Could not initialize class com.sun.xml.bind.v2.runtime.reflect.opt.Injector
Caused by: java.lang.NoClassDefFoundError: Could not initialize class org.springframework.cglib.proxy.Enhancer
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @5db4c359
view raw error3.xml hosted with ❤ by GitHub

This error is because Spring is trying to modify the access level of a class inside the java.lang package in the java.base module. Adding --add-opens java.base/java.lang=ALL-UNNAMED to your command-line will solve this issue. Same as for the --add-modules flag, the --add-opens flag (and this will be valid for all flags) can be added to the surefire or compiler plugin (or even the MAVEN_OPTS environment variable as a last resort).

Last but not least, you may be trying to access a now private enforced package, in which case you'll get something like this:

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make public final com.sun.xml.internal.bind.v2.runtime.JaxBeanInfo com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getBeanInfo(java.lang.Class) accessible:
module java.xml.bind does not "exports com.sun.xml.internal.bind.v2.runtime" to unnamed module @b968a76
view raw exports.error hosted with ❤ by GitHub

Adding --add-exports=java.xml.bind/com.sun.xml.internal.bind.v2.runtime=ALL-UNNAMED to your command-line will solve this issue.

I hope this post has helped you upgrade to Java 9! Enjoy ;)


Comments

Popular Posts