Java 10 is here, and the ecosystem is adapting. If you want to run Spring Boot apps in Java 10, the good news is it works fine (mostly), so here is how to get started as quickly as possible without stumbling on the newbie errors (like I did).
You can download the OpenJDK builds or you can use SDK Man:
$ sdk list java
================================================================================
Available Java Versions
================================================================================
10.0.2-oracle
> * 10.0.1-zulu
10.0.0-openjdk
...
8u131-zulu
7u141-zulu
6u93-zulu
$ sdk use java 10.0.1-zulu
This downloads and installs the JDK at ~/.sdkman/candidates/10.0.1-zulu
and puts it on your PATH
:
$ java -version
openjdk version "10.0.1" 2018-04-17
OpenJDK Runtime Environment Zulu10.2+3 (build 10.0.1+9)
OpenJDK 64-Bit Server VM Zulu10.2+3 (build 10.0.1+9, mixed mode)
If you are an Eclipse user, you might want to install the Java 9 support from the Eclipse Marketplace (make sure you have Eclipse Oxygen 4.7 first). You don’t have to run Eclipse with Java 9, but if you do you might need to change the ini file. Once you have the Java 9 features installed you can add the JDK to your Preferences → Java → Installed JREs
.
You can use the source code from this repo, or you can generate an empty project for yourself. To generate a project, go to Spring Initializr and generate a default blank project using Spring Boot 2.0.3. Java 10 is an option for the generator so the pom.xml
should show the Java version:
<properties>
<java.version>10</java.version>
</properties>
The Maven wrapper that comes with the generated project, and the plugin versions in the parent POM all work with Java 10, so you can build a jar file on the command line:
$ ./mvnw clean install
...
$ ls target/*.jar
spring-boot-java9-0.0.1-SNAPSHOT.jar
The jar file is executable as usual (use …-exec.jar
if you are building from the source code of this README):
$ java -jar target/spring-boot-java9-0.0.1-SNAPSHOT.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.3.RELEASE)
...
It doesn’t do anything, so the main method just finishes and the JVM exits. All good. It might be faster than Java 8, but the effect is probably not measurable, and Java 8 performs better if you tweak it (see below).
Spring needs to use reflection, and through CGLib it results in an illegal access, which is logged as a warning on startup:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.springframework.cglib.core.ReflectUtils$1 (jar:file:/home/dsyer/dev/demo/workspace/demo/target/spring-boot-java9-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-core-5.0.0.BUILD-SNAPSHOT.jar!/) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of org.springframework.cglib.core.ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
You can switch this off by adding -illegal-access=deny
to the command line (the default in Java 10 is permit
). Here’s a Spring JIRA issue tracking changes related to this warning. You can also use Spring Boot 2.1.0 (currently in snapshots), which pulls in Spring 5.1.0 and then the warning will go away.
Java 10 supports the old command line format of Java 8, which is why the executable jar just works. We can also use an explicit class path and a main method, just to be sure that it works. First we need to generate a classpath. A useful tool for that is the Thin Jar Launcher, and in particular the ThinJarWrapper
which can be downloaded from Maven Central:
$ wget http://repo1.maven.org/maven2/org/springframework/boot/experimental/spring-boot-thin-wrapper/1.0.12.RELEASE/spring-boot-thin-wrapper-1.0.12.RELEASE.jar
and then we can run the wrapper with some command line arguments to make it print the classpath for the current project:
$ CP=`java -jar spring-boot-thin-wrapper-1.0.12.RELEASE.jar --thin.archive=. --thin.classpath`
Then we can run our app like this (assuming the default class name from start.spring.io):
$ java -cp target/classes/:$CP com.example.demo.DemoApplication
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.3.RELEASE)
...
Note
|
You can use any tool you like to list the dependencies of your app and format them as a classpath, just get it in the right format and it will work the same. |
At this point we might decide to repackage the jar file into a thin (module) format, removing the spring-boot-maven-plugin
from the pom.xml
and re-building the project. Then we can use the jar itself on the classpath, instead of the target/classes
directory:
$ ./mvnw clean install
$ java -cp target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP com.example.demo.DemoApplication
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.3.RELEASE)
...
Java 10 has other features for running applications. So instead of using the classpath we can use the Jigsaw (module system) features, relying on "automatic modules" to turn all the dependency jars into modules. The command line is slightly different, but the result is the same (except that the WARN logs are not emitted):
java -p target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP --add-modules ALL-DEFAULT -m spring.boot.java9/com.example.demo.DemoApplication
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot ::
...
The app runs as before, but note that the Spring Boot version information is not printed because it is not accessible the same way from inside Spring Boot. Here are the pieces of the command line, blow by blow…
The module path is -p
(not -cp
) but it is in the same format as a classpath. Automatic modules only work as jars, which is why we built the non-executable jar for the app, instead of using target/classes
. If you try using a directory in the module path that isn’t a module you will find that it is not automatically converted to a module, and there will be an error on the command line:
$ java -p target/classes:$CP --add-modules ALL-DEFAULT -m spring.boot.java9/com.example.demo.DemoApplication
Error occurred during initialization of boot layer
java.lang.module.FindException: Module demo not found
We need to add additional modules to the command line since the default is a much narrower subset that won’t work with Spring Boot. If we ommit the --add-modules ALL-DEFAULT
it breaks:
$ java -p target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP -m spring.boot.java9/com.example.demo.DemoApplication
Exception in thread "main" java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationContextInitializer : org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.createSpringFactoriesInstances(SpringApplication.java:439)
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.getSpringFactoriesInstances(SpringApplication.java:418)
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.getSpringFactoriesInstances(SpringApplication.java:409)
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.<init>(SpringApplication.java:266)
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.<init>(SpringApplication.java:247)
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.run(SpringApplication.java:1245)
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.run(SpringApplication.java:1233)
at demo@0.0.1-SNAPSHOT/com.example.demo.DemoApplication.main(DemoApplication.java:10)
Caused by: java.lang.NoClassDefFoundError: java/sql/SQLException
at spring.beans@5.0.6.RELEASE/org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:176)
at spring.boot@2.0.2.RELEASE/org.springframework.boot.SpringApplication.createSpringFactoriesInstances(SpringApplication.java:435)
... 7 more
Caused by: java.lang.ClassNotFoundException: java.sql.SQLException
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)
... 9 more
With a classpath, or with Java 8, we provide the main class as a command line argument, unqualified with no option flag.
With a module path, i.e. using Jigsaw, we need to provide a -m …
which is in the form <module>/<mainclass>
. If you forget that, you get a rather unhelpful error:
$ java -p target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP --add-modules ALL-DEFAULT com.example.demo.DemoApplication
Error: Could not find or load main class com.example.demo.DemoApplication
Caused by: java.lang.ClassNotFoundException: com.example.demo.DemoApplication
The module name is the "automatic" one (the jar name with "." instead of "-").
With Spring Boot it’s easy to add features. A basic webapp with Tomcat can be created just by adding spring-boot-starter-web
to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
or you can add the Actuator using spring-boot-starter-actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Remember to set endpoints.default.web.enabled=true
if you want to see the Actuator endpoints in the webapp by default. E.g:
$ java -p target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP --add-modules ALL-DEFAULT -m spring.boot.java9/com.example.demo.DemoApplication --endpoints.default.web.enabled=true
...
2017-09-08 11:49:28.011 INFO 22102 --- [ main] b.e.w.m.WebEndpointServletHandlerMapping : Mapped "{[/application/mappings],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.endpoint.web.mvc.WebEndpointServletHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2017-09-08 11:49:28.011 INFO 22102 --- [ main] b.e.w.m.WebEndpointServletHandlerMapping : Mapped "{[/application/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.endpoint.web.mvc.WebEndpointServletHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2017-09-08 11:49:28.011 INFO 22102 --- [ main] b.e.w.m.WebEndpointServletHandlerMapping : Mapped "{[/application/status],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.endpoint.web.mvc.WebEndpointServletHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
...
JLink is a JDK tool that creates a self-contained binary image for a Java program (no need for a JRE or JDK at runtime). It works with Jigsaw modules, but only with explicit modules, not automatic ones, and most of the dependencies in a Spring Boot application only provide automatic modules. So this is the kind of thing you will see if you try to build an image:
$ cp target/spring-boot-java9-0.0.1-SNAPSHOT.jar target/demo.jar
$ jlink -p target/demo.jar:$CP:$JAVA_HOME/jmods --add-modules demo --output jre
Error: automatic module cannot be used with jlink: snakeyaml from file:///.../.m2/repository/org/yaml/snakeyaml/1.19/snakeyaml-1.19.jar
AOT is an (unsupported) feature of Java 9. There are plenty of "Hello World" examples out there, which seem to show improved startup performance. Nothing much to show how to work with non-trivial apps. Here’s a recipe:
$ CP=`java -jar spring-boot-thin-wrapper-1.0.12.RELEASE.jar --thin.archive=target/spring-boot-java9-0.0.1-SNAPSHOT.jar --thin.classpath`
$ java -XX:DumpLoadedClassList=target/app.classlist -cp $CP com.example.demo.DemoApplication
$ jaotc --output target/libDemo.so -J-cp -J$CP `cat target/app.classlist | sed -e 's,/,.,g'`
$ java -XX:AOTLibrary=target/libDemo.so -cp $CP com.example.demo.DemoApplication
You can add -XX:+PrintAOT
to the java
command line to see the classes being loaded from the shared library. Without that extra logging it’s about 15% faster (500ms compared to 600ms). Adding Tomcat and actuators shows 2000ms compared to 2400. But that was without any other command line arguments; once we add -noverify -XX:TieredStopAtLevel=1
the improvement isn’t so dramatic (1490ms vs 1540ms) with actuator, or without (560ms vs 630ms).
The above commands only compile the class from the boot classpath though (i.e. the JRE), so maybe including the application classes would speed things up enough to make it worthwhile. We have to use the Oracle JDK and some extra flags to get the app classes (-XX:+UnlockCommercialFeatures -XX:+UseAppCDS
). Unfortunately when you do that jaotc
fails because it can’t compile some of the classes (AOP proxies, and ones that refer to stuff that isn’t on the classpath). This is kind of a showstopper for a Spring Boot app because we use proxies a lot and also compile in references to classes that aren’t used at runtime. Even if there was a way to automatically filter out the classes that would fail this way, the limitations listed in the Oracle documentation and associated articles make it seem unlikely that it would be hugely successful in practice.
Running with Java 8 (no AOT) is the same or faster, though, so there’s not much incentive to use Java 9 (520ms for the vanilla app, 1490ms with actuator). Spring Boot 1.5.9 makes it a bit faster too, so (460ms and 1300ms).
Common Data Sharing extends to application classes in Java 10. It’s quite a bit quicker on startup (but Java 10 is slower, so Java 8 might be comparable overall):
$ CP=`java -jar spring-boot-thin-wrapper-1.0.12.RELEASE.jar --thin.archive=target/spring-boot-java9-0.0.1-SNAPSHOT.jar --thin.classpath`
$ java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst -cp target/classes/:$CP com.example.demo.DemoApplication
$ java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst -XX:SharedArchiveFile=hello.jsa -cp $CP com.example.demo.DemoApplication
$ java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa -cp $CP com.example.demo.DemoApplication