With the coming of Java 9, there is a lot of buzz on how to migrate applications to use the module system. Unfortunately, most of the articles written focus on simple Hello world applications. Or worse, regarding Spring applications, the sample app uses legacy practices - like XML for example. This post aims to correct that by providing a step-to-step migration guide for a non-trivial modern Spring Boot application. The sample app chosen to do that is the Spring Pet clinic.
There are basically 2 steps to use Java 9: first, be compatible then use the fully-fledged module system. This post aims at the former, a future post will consider the later.
Bumping the Java version
Once JDK 9 is available on the target machine, the first move is to bump the java.version
from 8 to 9 in the POM:
<properties>
<!-- Generic properties -->
<java.version>9</java.version>
</propertie>
Now, let’s mvn clean compile
.
Cobertura’s failure
The first error along the way is the following:
[ERROR] Failed to execute goal org.codehaus.mojo:cobertura-maven-plugin:2.7:clean (default) on project spring-petclinic: Execution default of goal org.codehaus.mojo:cobertura-maven-plugin:2.7:clean failed: Plugin org.codehaus.mojo:cobertura-maven-plugin:2.7 or one of its dependencies could not be resolved: Could not find artifact com.sun:tools:jar:0 at specified path /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/../lib/tools.jar -> [Help 1]
Cobertura is a free Java code coverage reporting tool.
It requires access to the tools.jar that is part of JDK 8 (and earlier). One of the changes in Java 9 is the removal of that library. Hence, that is not compatible. This is already logged as an issue. Given the last commit on the Cobertura repository is one year old, just comment out the Cobertura Maven plugin. And think about replacing Cobertura for JaCoCo instead.
Wro4J’s failure
The next error is the following:
[ERROR] Failed to execute goal ro.isdc.wro4j:wro4j-maven-plugin:1.8.0:run (default) on project spring-petclinic: Execution default of goal ro.isdc.wro4j:wro4j-maven-plugin:1.8.0:run failed: An API incompatibility was encountered while executing ro.isdc.wro4j:wro4j-maven-plugin:1.8.0:run: java.lang.ExceptionInInitializerError: null
wro4j is a free and Open Source Java project which will help you to easily improve your web application page loading time. It can help you to keep your static resources (js & css) well organized, merge & minify them at run-time (using a simple filter) or build-time (using maven plugin) and has a dozen of features you may find useful when dealing with web resources.
This is referenced as a Github issue. Changes have been merged, but the issue is still open for Java 9 compatibility should be part of the 2.0 release.
Let’s comment out Wro4J for the moment.
Compilation failure
Compiling the project at this point yields the following compile-time errors:
/Users/i303869/projects/private/spring-petclinic/src/main/java/org/springframework/samples/petclinic/vet/Vet.java Error:(30, 22) java: package javax.xml.bind.annotation is not visible (package javax.xml.bind.annotation is declared in module java.xml.bind, which is not in the module graph) /Users/i303869/projects/private/spring-petclinic/src/main/java/org/springframework/samples/petclinic/vet/Vets.java Error:(21, 22) java: package javax.xml.bind.annotation is not visible (package javax.xml.bind.annotation is declared in module java.xml.bind, which is not in the module graph) Error:(22, 22) java: package javax.xml.bind.annotation is not visible (package javax.xml.bind.annotation is declared in module java.xml.bind, which is not in the module graph)
That means code on the classpath cannot access this module by default.
It needs to be manually added with the --add-modules
option of Java 9’s javac
.
Within Maven, it can be set by using the maven-compiler-plugin:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<compilerArgs>
<arg>--add-modules</arg>
<arg>java.xml.bind</arg>
</compilerArgs>
</configuration>
</plugin>
Now the project can compile.
Test failure
The next step sees the failure of unit tests to fail with mvn test
.
The cause is the same, but it’s a bit harder to find. It requires checking the Surefire reports. Some contain exceptions with the following line:
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException
Again, test code cannot access the module. This time, however, the maven-surefire-plugin needs to be configured:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
<configuration>
<argLine>--add-modules java.xml.bind</argLine>
</configuration>
</plugin>
This makes the tests work.
Packaging failure
If one thinks this is the end of the road, think again. The packaging phase also fails with a rather cryptic error:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-jar-plugin:2.6:jar (default-jar) on project spring-petclinic: Execution default-jar of goal org.apache.maven.plugins:maven-jar-plugin:2.6:jar failed: An API incompatibility was encountered while executing org.apache.maven.plugins:maven-jar-plugin:2.6:jar: java.lang.ExceptionInInitializerError: null ... Caused by: java.lang.ArrayIndexOutOfBoundsException: 1 at org.codehaus.plexus.archiver.zip.AbstractZipArchiver.<clinit>(AbstractZipArchiver.java:116)
This one is even harder to find:
it requires a Google search to stumble upon the solution.
The plexus-archiver
is to blame.
Simply bumping the maven-jar-plugin
to the latest version - 3.2 at the time of this writing will make use of a Java 9 compatible version of the archiver and will solve the issue:
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
Spring Boot plugin failure
At this point, the project finally can be compiled, tested and packaged.
The next step is to run the app through the Spring Boot Maven plugin i.e. mvn spring-boot:run
.
But it fails again…:
[INFO] --- spring-boot-maven-plugin:1.5.1.RELEASE:run (default-cli) @ spring-petclinic --- [INFO] Attaching agents: [] Exception in thread "main" java.lang.ClassCastException: java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to java.base/java.net.URLClassLoader at o.s.b.devtools.restart.DefaultRestartInitializer.getUrls(DefaultRestartInitializer.java:93) at o.s.b.devtools.restart.DefaultRestartInitializer.getInitialUrls(DefaultRestartInitializer.java:56) at o.s.b.devtools.restart.Restarter.<init>(Restarter.java:140) at o.s.b.devtools.restart.Restarter.initialize(Restarter.java:546) at o.s.b.devtools.restart.RestartApplicationListener.onApplicationStartingEvent(RestartApplicationListener.java:67) at o.s.b.devtools.restart.RestartApplicationListener.onApplicationEvent(RestartApplicationListener.java:45) at o.s.c.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:167) at o.s.c.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139) at o.s.c.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:122) at o.s.b.context.event.EventPublishingRunListener.starting(EventPublishingRunListener.java:68) at o.s.b.SpringApplicationRunListeners.starting(SpringApplicationRunListeners.java:48) at o.s.b.SpringApplication.run(SpringApplication.java:303) at o.s.b.SpringApplication.run(SpringApplication.java:1162) at o.s.b.SpringApplication.run(SpringApplication.java:1151) at org.springframework.samples.petclinic.PetClinicApplication.main(PetClinicApplication.java:32)
This is a documented issue that Spring Boot Dev Tools v1.5 is not compatible with Java 9.
Fortunately, this bug is fixed in Spring Boot 2.0.0.M5. Unfortunately, this specific version is not yet available at the time of this writing. So for now, let’s remove the dev-tools and try to run again. It fails again, but this time the exception is a familiar one:
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException
Let’s add the required argument to the spring-boot-maven-plugin:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>--add-modules java.xml.bind</jvmArguments>
</configuration>
...
</plugin>
The app can finally be be launched and is accessible!
Conclusion
Running a non-trivial legacy application on JDK 9 requires some effort. Worse, some important features had to be left on the way: code coverage and web performance enhancements. On the opposite side, the only meager benefit is the String memory space improvement. In the next blog post, we will try to improve the situation to actually make use of modules in the app.