I upgraded to Java 9 - here's what happened

I recently participated in a Twitter conversation about upgrading to Java 9. Like most of the people, my current projects are all on Java 8. Of course the question came up, why don’t you upgrade? Good question! Since I hadn’t even tried, I decided to see how far I could come…

The component I took for my experiment is a pretty simple Spring application. It doesn’t use Spring Boot. It exposes a few REST endpoints, and calls couple of webservices over SOAP. For these webservices, it uses bindings generated by the JAXB plugin for Maven.

Compiling with and against Java 9

Since I use Maven for building, my first step was to tell Maven to compile against Java 9:

<properties>
  <maven.compiler.source>1.9</maven.compiler.source>
  <maven.compiler.target>1.9</maven.compiler.target>
</properties>

Since my main development work is (unfortunately) on Windows, I can’t use my own trick for rapid switching. As said, I normally use Java 8, and invoking Maven with Java 8 gives me:

Error:java: invalid source release: 1.9.

Of course, JDK 8 doesn’t know how to compile against Java 9. So to instruct Maven to use Java 9, I did:

set JAVA_HOME="C:\Progra~1\Java\jdk-9.0.1"

Invoking mvn clean package again, I now got:

package javax.xml.bind is not visible
package javax.annotation is not visible

It is a consequence of code in Java SE that’s actually from Java EE. In this case, it’s the java.xml.bind module that we need. It is still present in the Java library, but how can I get access to it? There’s a few options:

  1. Add a dependency to the JAXB API. Note that this is only the API, no implementation is there yet. Although this will compile, it is unlikely that it will work. And it’s a bit stupid, since JAXB is still in the Java library.
  2. Completely modularise the code base. By doing this, I need to think of modules and how I’m naming them. This might be a lot of work, but of course it is the best option in the long run. After that, I could add requires java.xml.bind to the module descriptor.
  3. Tell the Java compiler and the runtime to add modules (and their dependencies) to the module graph. This is useful for modules that would otherwise not show up. Seems like a nice approach!

Adding modules to the compilation process

To make sure this change will not affect people who use Java 8, I made the changes in a separate Maven profile. It is activated by the version of the JDK, 9 in this case. I added the following to the POM:

<profiles>
  <profile>
    <id>java-9</id>
    <activation>
      <jdk>9</jdk>
    </activation>
    <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.6.0</version>
          <configuration>
            <compilerArgs>
              <arg>--add-modules=java.activation,java.xml.bind,java.xml.ws,java.xml.ws.annotation</arg>
            </compilerArgs>
          </configuration>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>

This snippet tells Maven: if we’re running on JDK 9, invoke javac with an additional switch --add-modules=.... Now the code compiles!

Running with Java 9

But compilation alone isn’t enough - we need to be able to run the code as well! The first run of your code is the unit tests - you do write unit tests, don’t you? We do, and the run fails miserably:

java.lang.NoClassDefFoundError: javax/xml/soap/SOAPException
	at AbstractWebserviceClientTest.<init>(AbstractWebserviceClientTest.java:21)

Seems like the message we saw before, so I added the java.xml.ws module to the POM as well. That didn’t make any difference… How come?

The answer is in the fact that I’m now running the code instead of compiling it. Adding --add-modules=java.xml.ws affects the compilation, but not the execution of the code. Instead, I need to tell the Surefire plugin to add this module. I’m extending the java-9 profile for that, with this snippet:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <argLine>--add-modules=java.xml.ws</argLine>
  </configuration>
</plugin>

And now I’m happy, because I see:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Great, good luck so far!

Restoring test coverage reports

Until now, I cheated a bit, because in my original POM, the <argLine> used to be ${surefireArgLine}. This variable is set by invoking jacoco-maven-plugin:prepare-agent. Setting the <argLine> to ${surefireArgLine} --add-modules=java.xml.ws results in a lot of test failures. Most of them look like this:

ERROR!
java.lang.IllegalAccessError: Update to static final field SpringIntegrationTest.$jacocoData attempted from a different method ($jacocoInit) than the initializer method <clinit> 
	at SpringIntegrationTest.$jacocoInit(SpringIntegrationTest.java)
	at SpringIntegrationTest.<init>(SpringIntegrationTest.java)

And that’s the whole stacktrace! It looks like some JaCoCo-related issue, but I can’t really figure out why or how. We use JaCoCo to measure which parts of our code are covered by unit tests, and which are not. Upgrading to the lastest version of the jacoco-maven-plugin doesn’t make a difference. Running the very same test in IntelliJ doesn’t give a stacktrace. But when I add the surefireArgLine, I can see the same error. But this time, the stacktrace is longer:

java.lang.IllegalAccessError: Update to static final field SpringIntegrationTest.$jacocoData attempted from a different method ($jacocoInit) than the initializer method <clinit> 

	at SpringIntegrationTest.$jacocoInit(SpringIntegrationTest.java)
	at SpringIntegrationTest.<init>(SpringIntegrationTest.java)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:488)
	at org.junit.runners.BlockJUnit4ClassRunner.createTest(BlockJUnit4ClassRunner.java:217)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:227)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:287)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)

Interesting: maybe I should blame Spring Test? Or actually, blame myself, since I’m not using the latest version of Spring? Upgrading to the last 4.x-release doesn’t make this error go away. Still no luck; to be continued!

Running the application itself

Let’s skip the test coverage for a while, and see if the application actually works with Java 9! Starting Tomcat seems to work. When I deploy the application and watch its logging while Spring boots, it ends with

Caused by: java.lang.ClassNotFoundException: javax.xml.soap.SOAPException
  at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1284)
  at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1118)
	... 113 common frames omitted

Seems familiar, so I add a bunch of --add-modules=... to the Tomcat start command and restart it. There’s a bunch of warnings about illegal reflective access, and one about a missing class sun.misc.GC. Despite all of them, I am happy to see

Artifact my-application:war exploded: Artifact is deployed successfully
Artifact my-application:war exploded: Deploy took 8,328 milliseconds

Running a few manual testcases, it seems that it works fine. That is absolutely good news!

Conclusion

So, I managed to upgrade one application to Java 9. Completely happy? No: I didn’t manage to have unit test coverage reports. These reports help me in assessing the software quality, so I don’t want to live without them. The warning about sun.misc.GC needs investigation - maybe it’s just a matter of upgrading Tomcat.

Also, the bunch of warnings about illegal reflective access need to be fixed. Warnings are errors in the making, and in this case, that is certainly true. In the original proposal, Mark Reinhold already said:

[…] I hereby propose to allow illegal reflective access from code on the class path by default in JDK 9, and to disallow it in a future release.

Before deploying this app with Java 9 to a production environment, I would want to run a full integration test. Since we do that anyway in our team, it is of course automated.

Further reading