A Practical Guide to Java 9 Migration
This handy guide will help you migrate your Java 8 apps to Java 9, including steps and advice for bringing your applications to modules.
Join the DZone community and get the full member experience.Join For Free
This article is a practical example of migrating a Java application from 1.8 to 9. It walks through the steps and different problems and solutions of migrating a Java application to 9 and to modules. For clarification, this article is not supposed to cover all aspects of Java 9 adoption, but rather focuses on this example and the problems related to it.
The example used in this article is a simple implementation of the CQRS design pattern that I have extracted from the repository java-design-pattern.
The repository can be found here. It contains four branches, each branch containing scripts and configurations for compiling and running the application. Note that we'll be using two ways of compiling and running the application — command line and Maven. Also note that I've configured a repository for downloading Maven dependencies in order to avoid any surprises.
All the errors encountered in the migration process can be found here. Every error is attached to an id that I will make reference to from the article.
The migration to Java 9 is done with respect to the following approach:
Start with a Java 1.8 application (master branch).
Move the application to Java 9 using classpath (java9-classpath branch).
Make the application a module and move it with its direct dependencies to the modulepath (java9-module-v1 branch).
Move the application (module) and its dependencies to the modulepath (java9-module-v2 branch).
Here, we'll use the classpath for compiling and running our application, which means all of our code and its dependencies will be put in the unnamed module.
After running the application, you'll get the error BR-CP-ERR-01. The cause of this error is that JAXB is one of the Java EE modules that was removed in Java 9 by default from the set of root modules. To fix that, we can add the option --add-modules java.xml.bind. This can be used as a temporary solution, but the recommended solution is to add the JAXB library as a standalone dependency.
In the project, you'll find that I've added the JAXB dependencies to the pom.xml file, and I've changed the Java version of the Maven compiler plugin. With Java 9, we can remove the -source and -target and use the --release option instead. Notice also that I've upgraded the versions of the Maven compiler plugin and the JaCoCo Maven plugin to the latest version to fix some failures.
To run the application, we simply use the command:
java -cp "mytarget/lib/*":mytarget/cqrs.jar com.iluwatar.cqrs.app.App
Right now, our application is running against Java 9, but it's not yet a module. But we could choose a name for our application and put it in the META-INF/MANIFEST.MF file. This step is important if we were library maintainers. You can also do this using the Maven JAR plugin and specify the automatic-module-name, like you'll find in the java9-classpath branch of the repository.
In order to turn our application into a module, we'll start by running jdeps. In the project, you'll find a script that uses jdeps and puts the results in a file in the temp directory. The script will also use the same command to generate the module-info. The module-info generated will add some of the 'requires' (the modules that our application depends upon) and will export all of our packages. Here, I took the chance to refactor the packages and then exported just the packages that I wanted to export.
Next, we'll move to compilation. Notice that I've put (in the command line script) our module and its direct dependencies in the modulepath and the other dependencies in the classpath. I chose to do so because that's how Maven works. Maven will look for the module-info and put the modules declared in the modulepath.
After compiling our application, the first error that we'll see is BR-V1-ERR-02. It turns out that the SessionFactory interface of Hibernate extends the Referenceable interface java.naming. So, we'll need to add this module in the requires area.
Then, when trying to run the application, we'll have the three following errors: BR-V1-ERR-02, BR-V1-ERR-03, and BR-V1-ERR-04. The first one complains that our application cannot find the SQLException class. The solution for this is to add the option --add-modules java.sql. The second error is caused by Hibernate trying to reflect over our objects, so we need to open the entities in the module descriptor. The last one complains that our module is trying to access javassist, which exist in the unnamed module.
There are two solutions. The first one is to add the option --add-reads com.iluwatar.cqrs=ALL-UNNAMED, which will run the application successfully. The second solution (the recommended one) is to move javassist from the unnamed module and put it in the modulepath as an automatic module. And because of the BR-V1-ERR-06 error, we'll need to add the option --add-opens java.base/java.lang=javassist.
In the end, we'll have the following command:
java --add-modules java.sql \ --add-opens java.base/java.lang=javassist \ -p mytarget/lib-direct:mytarget/cqrs.jar \ -cp "mytarget/lib/*" \ -m com.iluwatar.cqrs/com.iluwatar.cqrs.app.App
Once we get this to work using the command line, Maven shouldn't cause many problems. I didn't use the Maven exec plugin because I had some mixing of the classpath and the modulepath. The only other problem that I needed to fix is BR-V1-ERR-06. Because the Maven checkstyle plugin doesn't recognize the module descriptor, we need to exclude it using the plugin configuration.
In this section, we'll try to run our application without using the classpath. That means all of the libraries must be put in the modulepath.
After trying to compile the application, we'll get the error BR-V2-ERR-01. According to the error message, we have split packages. So we'll use the option --patch-module jaxb.core=mytarget/patch-modules/jaxb-runtime-2.3.0.jar to fix the error.
At runtime, we'll get other errors: BR-V2-ERR-02 and BR-V2-ERR-03. The first one complains about the name of the JAR, so we need to change the name of the library geronimo.jta.1.1.spec (remove the numbers from the module name). In the second one, we can see that we have other split packages.
If we try to use --patch-module again, we'll get another error at runtime saying that we have yet another split package between java.xml and xml.api. Here, we need to take a minute and analyze the packages of the three modules:
As we can see, we have a lot of non-empty common packages. We can't use --patch-module to put both xml.api and stax.api into java.xml because we are only allowed to that once for a certain module. So I decided to exclude those dependencies (as you can see in the pom.xml) and then require java.sql in the module descriptor.
Of course, we could've just required java.xml, but we already know that our module needs java.sql and this will transitively require java.xml. Then, we'll have to deal with other errors starting from BR-V2-ERR-04 to BR-V2-ERR-07. But those are nothing that we didn't have previously. So in the end, we can run our module with the following command:
java \ --patch-module jaxb.core=mytarget/patch-modules/jaxb-runtime-2.3.0.jar \ --patch-module java.sql=mytarget/patch-modules/geronimo-jta_1.1_spec-1.1.1.jar \ --add-modules jdk.unsupported \ --add-opens java.base/java.lang=javassist \ --add-exports java.sql/javax.transaction=hibernate.core \ -p mytarget/lib:mytarget/cqrs.jar \ -m com.iluwatar.cqrs/com.iluwatar.cqrs.app.App
Last, I used jlink to create a small Java runtime, ship it, and run it inside a Docker image. There is a script named java9-dockerized-cqrs that can do just that.
As you've seen, migrating to Java 9 is straightforward most of the time, but on the other hand, migrating to modules can be frustrating. I think that, though migrating to modules causes many problems, you'll be able to better control your application and better understand your dependencies and libraries.
Opinions expressed by DZone contributors are their own.