Last week, I tried to make a Spring Boot app - the famous Pet Clinic, Java 9 compatible. It was not easy. I had to let go of a lot of features along the way. And all in all, the only benefit I got was improvement of String memory management.
This week, I want to continue the migration by fully embracing the Java 9 module system.
Configuring module meta-data
Module information in Java 9 is implemented through a module-info.java
file.
The first step is to create such a file at the root of the source directory, with the module name:
module org.springframework.samples.petclinic {
}
The rest of the journey can be heaven or hell. I’m fortunate to benefit from an IntelliJ IDEA license. The IDE tells exactly what class it cannot read and a wizard lets you put it in the module file. In the end, it looks like that:
module org.springframework.samples.petclinic {
requires java.xml.bind;
requires javax.transaction.api;
requires validation.api;
requires hibernate.jpa;
requires hibernate.validator;
requires spring.beans;
requires spring.core;
requires spring.context;
requires spring.tx;
requires spring.web;
requires spring.webmvc;
requires spring.data.commons;
requires spring.data.jpa;
requires spring.boot;
requires spring.boot.autoconfigure;
requires cache.api;
}
Note that module configuration in the maven-compiler-plugin
and maven-surefire-plugin
can be removed.
Configuration the un-assisted way
If you happen to be in a less than ideal environment, the process is the following:
- Run
mvn clean test
- Analyze the error in the log to get the offending package
- Locate the JAR of said package
- If the JAR is a module, add the module name to the list of required modules
- If not, compute the automatic module name, and add it to the list of requires modules
For example:
[ERROR] ~/spring-petclinic/src/main/java/org/springframework/samples/petclinic/system/CacheConfig.java:[21,16] package javax.cache is not visible [ERROR] (package javax.cache is declared in the unnamed module, but module javax.cache does not read it)
javax.cache
is located in the cache-api-1.0.0.jar
.
It’s not a module since there’s no module-info
in the JAR.
The automatic module name is cache.api
.
Write it as a requires
in the module.
Rinse and repeat.
ASM failure
Since the first part of this post, I’ve been made aware that Spring Boot 1.5 won’t be made Java 9-compatible. Let’s do it.
Bumping Spring Boot to 2.0.0.M5 requires some changes in the module dependencies:
hibernate.validator
toorg.hibernate.validator
validation.api
tojava.validation
And just when you think it might work:
Caused by: java.lang.RuntimeException at org.springframework.asm.ClassVisitor.visitModule(ClassVisitor.java:148)
This issue has already been documented. At this point, explicitly declaring the main class resolves the issue.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>--add-modules java.xml.bind</jvmArguments>
<mainClass>org.springframework.samples.petclinic.PetClinicApplication</mainClass>
</configuration>
...
</plugin>
Javassist failure
The app is now ready to be tested with mvn clean spring-boot:run
.
Unfortunately, a new exception comes our way:
2017-10-16 17:20:22.552 INFO 45661 --- [ main] utoConfigurationReportLoggingInitializer : Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled. 2017-10-16 17:20:22.561 ERROR 45661 --- [ main] o.s.boot.SpringApplication : Application startup failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.boot.archive.spi.ArchiveException: Could not build ClassFile
A quick search redirects to an incompatibility between Java 9 and Javassist. Javassist is the culprit here. The dependency is required Spring Data JPA, transitively via Hibernate. To fix it, exclude the dependency, and add the latest version:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<artifactId>javassist</artifactId>
<groupId>org.javassist</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
<scope>runtime</scope>
</dependency>
Fortunately, versions are compatible - at least for our usage.
It works!
We did it! If you arrived at this point, you deserve a pat on the shoulder, a beer, or whatever you think you deserve.
Icing on the cake, the Dev Tools dependency can be re-added.
Conclusion
Migrating to Java 9 requires using Jigsaw, whether you like it or not. At the very least, it means a painful trial and error search-for-the-next-fix process and removing important steps in the build process. While it’s interesting for library/framework developers to add an additional layer of access control to their internal APIs, it’s much less so for application ones. At this stage, it’s not worth to move to Java 9.
I hope to conduct this experiment again in some months and to notice an improvement in the situation.