Upgrade to Java 10 now! Why not?

How to migrate to module system step-by-step

Leonardo Zanivan
criciumadev
6 min readMay 18, 2018

--

Update: Jump to Java 11

TL;DR;

  • Describes the benefits of upgrading the application to module system.
  • Migration can be done incrementally: run, compile, modularize.
  • Full source code and upcoming posts are available at the bottom.

After the release of Java 9 and now 10, there is a lot of open questions on how to migrate applications to use the module system. Unfortunately, most of the articles written focus on simple Hello World applications.

The goal of this post is to describe a step-by-step migration guide for a non-trivial modern Spring Boot application. The sample app chosen to do that is the Spring PetClinic, a Spring Boot 2 sample application using WebMVC, Actuator, Cache, Data JPA, Thymeleaf and Test starters.

There are basically three incremental phases to fully migrate to Java 10:

  1. Run an existing Java application with JDK 10.
  2. Compile the application with Java 10.
  3. Modularize the application to use Module System.

If you are not ready to do all three at same time, do it in your time. This way, it will be easier to get familiar with the new version and migrate incrementally.

First you need to download and install JDK 10 for your operation system: http://jdk.java.net/10/

After you must update your favorite IDE to support Java 10:

Eclipse IDE: https://www.eclipse.org/downloads/

IntelliJ IDEA: https://www.jetbrains.com/idea/download/

1. Run an existing Java application with JDK 10

Why upgrade to JDK 10?

  • Full support for Linux containers (Docker included).
  • Support parallel full garbage collection on G1.
  • Application Class-Data Sharing feature open sourced.
  • Heap allocation on alternative memory devices.
  • New default set of root authority certificates.
  • JShell.

Running your application

This is a really simple step, the application (jars) created with earlier Java versions can run on JDK 10 without major issues, all they need do is to add java.se.ee module, because it contains all deprecated modules that are not included on the default classpath.

java --add-modules java.se.ee -jar myapp.jar

2. Compile the application with Java 10

Why upgrade to Java 10?

  • Local variable type inference (var keyword).
  • New native unmodifiable collections APIs.
  • New reactive streams APIs.
  • Improved streams APIs.
  • Improved process API.
  • Support for HTTP/2 (incubating).
  • Multi-release JARs.

Steps

  1. Clone Spring PetClinic repository.
git clone git@github.com:spring-projects/spring-petclinic.git

2. Open pom.xml and update java.version property.

<java.version>10</java.version>

3. Remove cobertura-maven-plugin references as it’s not supported with JDK 10 and looks like it isn't being maintained for a few years.

You can use JaCoCo 0.8.1 which supports JDK 10 instead.

4. Include mockito-core dependency inside wro4j-maven-plugin dependencies section.

<plugin>
...
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.18.0</version>
</dependency>

...
</plugin>

5. Update maven-compiler-plugin to version 3.7.0+ with latest asm dependency.

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<release>${java.version}</release>
</configuration>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.1.1</version>
</dependency>

</dependencies>
</plugin>

6. Update maven-surefire-plugin to version 2.21.0+ with latest asm dependency.

<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.21.0</version>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.1.1</version>
</dependency>

</dependencies>
</plugin>

7. Include java.xml.bind and javax.activation modules dependencies. This step is required because those modules are deprecated and will be removed by JEP-320 that is scheduled to be released with JDK 11.

<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
<version>1.2.0</version>
</dependency>

8. Run the application with spring-boot-maven-plugin.

./mvnw spring-boot:run

The application UI will be available at http://localhost:8080/

9. Package the application and run tests.

./mvnw clean package

10. Run the application using packaged executable jar.

java -jar target/spring-petclinic-2.0.0.BUILD-SNAPSHOT.jar

At this point you can compile and run your application with Java 10, but you are not using module system yet.

3. Modularize the application to use Module System

Why migrate to Module System?

  • Reliable configuration — to replace the brittle, error-prone class-path mechanism with a means for program components to declare explicit dependences upon one another.
  • Strong encapsulation — to allow a component to declare which of its public types are accessible to other components, and which are not.
  • Create a minimal JRE image for your application.
  • Decrease application memory footprint.
  • Optimize application startup time.

Steps

1. Create a file named module-info.java in src/main/java directory with the following contents:

module spring.petclinic {
}

Now when you try to compile the application and you will see a lot of errors like this:

Error:(19, 27) java: package org.springframework.boot is not visible
(package org.springframework.boot is declared in module spring.boot, but module spring.petclinic does not read it)

That means the application behaves as a modular layout and has to wire modules for compilation and runtime.

You can use Maven dependency plugin resolve goal to list all module names currently in the classpath and add them into module-info:

mvn compile org.apache.maven.plugins:maven-dependency-plugin:3.1.1:resolve

Notes: The command above doesn't exclude transitive dependencies or include JDK modules.

Unfortunately, jdeps won’t help you generate the module descriptor for many reasons, but mostly because third party libraries didn't add module system descriptors yet and they are treated as special automatic modules.

The final module descriptor should look like this:

open module spring.petclinic {    requires cache.api;    requires java.activation;
requires java.instrument;
requires java.persistence;
requires java.sql;
requires java.transaction;
requires java.validation;
requires java.xml.bind;
requires org.hibernate.validator; requires spring.beans;
requires spring.boot;
requires spring.boot.autoconfigure;
requires spring.context;
requires spring.core;
requires spring.data.commons;
requires spring.data.jpa;
requires spring.tx;
requires spring.web;
requires spring.webmvc;
}

Notes: open keyword is mandatory due to reflection requirements by Spring Framework and Hibernate JPA.

2. Include maven-jar-plugin to create application jar (classes only) and copy it to modules directory.

<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
</configuration>
</plugin>

3. Include maven-dependency-plugin to copy runtime dependencies to modules directory.

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
<includeScope>runtime</includeScope>
<excludeArtifactIds>
spring-boot-devtools,jaxb-api,jaxb-core,jaxb-runtime
</excludeArtifactIds>
</configuration>
</execution>
</executions>
</plugin>

Notes: spring-boot-devtools and java.xml.bind dependencies should be excluded from modules directory.

4. Include java.persistence and java.transaction module dependencies which fixes automatic modules issues with their updated versions:

<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.2.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
<version>1.1.1.Final</version>
</dependency>

Also, add a exclusion for old javax.transaction dependency from spring-boot-starter-data-jpa.

<exclusions>
<exclusion>
<artifactId>javax.transaction-api</artifactId>
<groupId>javax.transaction</groupId>
</exclusion>
</exclusions>

5. Modify maven-surefire-plugin configuration to disable forked process:

<configuration>
<forkCount>0</forkCount>
</configuration>

Notes: When module-info.java is present and fork process is enabled, surefire creates a mixed classpath with modules and unnamed modules causing module visibility issues and preventing the application to start.

6. Package and run the application with module system.

./mvnw clean packagejava --add-modules java.xml.bind \
--upgrade-module-path=target/modules \
--module spring.petclinic/org.springframework.samples.petclinic.PetClinicApplication

Notes: upgrade-module-path is required due to JDK modules overrides (java.activation and java.transaction). The extra add module for java.xml.bind is because a working JAXB dependency override isn't released yet.

7. You can get rid of main class specified with the module parameter if set module main-class attribute using the following command:

jar --update \
--file=target/modules/spring-petclinic-2.0.0.BUILD-SNAPSHOT.jar
--main-class=org.springframework.samples.petclinic.PetClinicApplication

8. In order to automate the previous step, you can add exec-maven-plugin:

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<id>module-main-class</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>jar</executable>
<arguments>
<argument>
--update
</argument>
<argument>
--file=${project.build.directory}/modules/${project.build.finalName}.jar
</argument>
<argument>
--main-class=org.springframework.samples.petclinic.PetClinicApplication
</argument>
<argument>
--module-version=${project.version}
</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>

9. Now you can run the application without explicit main-class declaration:

./mvnw clean packagejava --add-modules java.xml.bind \
--upgrade-module-path=target/modules \
--module spring.petclinic

Notes: This isn’t supported out of the box by Maven yet due to MJAR-238.

10. Let's test the application, you will soon find that it’s not fully working and an exception is thrown when rendering field validation messages:

java.lang.UnsupportedOperationException: ResourceBundle.Control not supported in named modules at java.base/java.util.ResourceBundle.checkNamedModule(ResourceBundle.java:1547) at java.base/java.util.ResourceBundle.getBundle(ResourceBundle.java:1508) at spring.context@5.0.4.RELEASE/org.springframework.context.support.ResourceBundleMessageSource.doGetBundle(ResourceBundleMessageSource.java:223)

This is caused because Spring Framework 5.0.5 and below rely on ResourceBundle.Control to manipulate properties file encoding, this isn’t supported by module system and is already fixed in version 5.0.6+.

This can be easily fixed adding the following property in pom.xml:

<spring.version>5.0.6.RELEASE</spring.version>

Notes: Because of JEP 226: UTF-8 Property Resource Bundles the default properties encoding is UTF-8 now and can be configurable via system property.

Congratulations!

Now you can migrate any application to Java 10 module system.

The full changes can be viewed in the following PR from my forked repo:

Part 2: Create a Cloud Native Image using Java Modules

--

--