Skip to content

Latest commit

 

History

History
289 lines (215 loc) · 17 KB

README.adoc

File metadata and controls

289 lines (215 loc) · 17 KB

Java 9 is here, and the ecosystem is adapting. If you want to run Spring Boot apps in Java 9, 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).

Install Java 9

You can download the OpenJDK builds or you can use SDK Man:

$ sdk list java
================================================================================
Available Java Versions
================================================================================
     9.0.1-zulu
     9.0.1-oracle
     9.0.0-zulu
     ...
     8u131-zulu
     7u141-zulu
     6u93-zulu
$ sdk use java 9.0.1-zulu

This downloads and installs the JDK at ~/.sdkmain/candidates/9.0.1-zulu and puts it on your PATH:

$ java -version
openjdk version "9.0.1.3"
OpenJDK Runtime Environment (Zulu build 9.0.1.3+11)
OpenJDK 64-Bit Server VM (Zulu build 9.0.1.3+11, 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.

Create a Spring Boot Application

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.2. Java 9 is an option for the generator so the pom.xml should show the Java version:

pom.xml
	<properties>
		<java.version>1.9</java.version>
	</properties>

The Maven wrapper that comes with the generated project, and the plugin versions in the parent POM all work with Java 9, 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:

$ java -jar target/spring-boot-java9-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v2.0.2.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 9 is permit). Here’s a Spring JIRA issue tracking changes related to this warning.

Run Using an Explicit Classpath

Java 9 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.9.RELEASE/spring-boot-thin-wrapper-1.0.9.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.9.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.2.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.2.RELEASE)

...

Running Using Jigsaw

Java 9 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:

Module Path

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

Adding JDK Modules

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

The Main Class

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 "-").

Adding More Features

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:

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

pom.xml
		<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:

$ jlink -p target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP:$JAVA_HOME/jmods --add-modules demo --output jre
Error: module-info.class not found for logback.core module

Ahead of Time Compilation (AOT)

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.9.RELEASE.jar --thin.archive=. --thin.classpath`
$ java -XX:DumpLoadedClassList=target/app.classlist -cp target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP com.example.demo.DemoApplication
$ jaotc --output target/libDemo.so -J-cp -Jtarget/spring-boot-java9-0.0.1-SNAPSHOT.jar:$CP `cat target/app.classlist | sed -e 's,/,.,g'`
$ java -XX:AOTLibrary=target/libDemo.so -cp target/spring-boot-java9-0.0.1-SNAPSHOT.jar:$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 30% faster (600ms compared to 900ms). 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.

Java 8

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).

Java 10

Java 10 is nearly here, so it’s worth checking that everything works. Here are a couple of things that may need tweaking:

Depending on whether it is used by your app or not, you may have to add the JAXB api jar explicitly to your classpath (Spring Boot 2.0 has a version for it in its dependency management, Spring Boot 1.5 does not):

		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
		</dependency>

For older versions of Spring Boot, some Maven plugin versions need managing as well, e.g:

		<maven-jar-plugin.version>3.0.2</maven-jar-plugin.version>
		<maven-surefire-plugin.version>2.19.1</maven-surefire-plugin.version>

CDS

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 quicker overall):

$ 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