The new spring-graalvm-native 0.7.1 & GraalVM 20.1.0 releases are full of optimizations! The configuration of the native-image command has become much easier. So let’s take a look at the native-image-maven-plugin for our Spring Boot GraalVM Native Image compilations.
Spring Boot & GraalVM – blog series
Part 1: Running Spring Boot apps as GraalVM Native Images
Part 2: Running Spring Boot GraalVM Native Images with Docker & Heroku
Part 3: Simplifying Spring Boot GraalVM Native Image builds with the native-image-maven-plugin
New 0.7.1 release of the Spring Feature & GraalVM 20.1.0
The Spring team is really moving fast! They released the new version 0.7.1
of the spring-graalvm-native project a few days ago and it again optimizes the way we compile our Spring Boot apps into GraalVM native images. If you want to know more about how to use it, feel encouraged to check out the first article of this blog series .
With the release of version
0.7.0
the Spring Feature project was renamed fromspring-graal-native
tospring-graalvm-native
! So don’t get confused while accessing the project, docs or downloading the newest Maven dependency from the Spring Milestones repository.
The latest release of the Spring experimental project spring-graalvm-native is now based on Spring Boot 2.3.0.RELEASE
and GraalVM 20.1.0
. It comes with improved support for Kotlin, Spring Data MongoDB and logging. Additionally it ships with dedicated functional Spring application support and an even more reduced memory footprint. For more details see this spring.io post . Also, the GraalVM team released the new GraalVM version 20.1.0
with lots of improvements – also covering Spring (see this post about GraalVM 20.1.0 release ).
The pom.xml of this blog series’ example project has already been updated. To use the new version, simply update the Maven dependency (and don’t forget to have the Spring Milestone repositories in place also):
1<dependencies> 2 <dependency> 3 <groupId>org.springframework.experimental</groupId> 4 <artifactId>spring-graalvm-native</artifactId> 5 <version>0.7.1</version> 6 </dependency> 7... 8</dependencies> 9 10 11<repositories> 12 <repository> 13 <id>spring-milestones</id> 14 <name>Spring Milestones</name> 15 <url>https://repo.spring.io/milestone</url> 16 </repository> 17</repositories> 18<pluginRepositories> 19 <pluginRepository> 20 <id>spring-milestones</id> 21 <name>Spring Milestones</name> 22 <url>https://repo.spring.io/milestone</url> 23 </pluginRepository> 24</pluginRepositories>
As we’re now also able to leverage Docker for our Spring Boot Native Image compilations , the example project’s Dockerfile now also uses the latest GraalVM release:
1FROM oracle/graalvm-ce:20.1.0-java11
Moving from compile scripts to the native-image-maven-plugin
The new release of the spring-graalvm-native project also comes with some more subtle changes under the hood that make compilation of Spring Boot apps into GraalVM Native Images much easier again. One of those changes is about the required configuration options for the native-image
command. Many of those parameters are now simply enabled by default . So we don’t need to explicitly define them anymore. Especially the --no-server
and --no-fallback
options can be left out using the new release. The final native-image
command for the example Spring Webflux application now looks like this (see the compile.sh of the example project for more details):
1GRAALVM_VERSION=`native-image --version` 2echo "[-->] Compiling Spring Boot App '$ARTIFACT' with $GRAALVM_VERSION" 3time native-image \ 4 -J-Xmx4G \ 5 -H:+TraceClassInitialization \ 6 -H:Name=$ARTIFACT \ 7 -H:+ReportExceptionStackTraces \ 8 -Dspring.graal.remove-unused-autoconfig=true \ 9 -Dspring.graal.remove-yaml-support=true \ 10 -cp $CP $MAINCLASS;
But having a simpler native-image
command in place, this could be a good time to take a look at the native-image-maven-plugin .
Don’t get confused about the package name of the org.graalvm.nativeimage.native-image-maven-plugin ! There’s also an older version of this plugin called com.oracle.substratevm.native-image-maven-plugin , which isn’t maintained anymore .
Using the native-image-maven-plugin
will mostly replace the steps 6., 7. & 8. described in the first post’s paragraph Preparing Spring Boot to be Graal Native Image-friendly . But it is still good to know what’s happening behind the scenes if something goes wrong. I think that’s also the reason the Spring team has a compile.sh
script in place for each of their sample projects .
Using the native-image-maven-plugin
In order to use the plugin, we extend our pom.xml with a Maven profile called native
like this:
1<profiles> 2 <profile> 3 <id>native</id> 4 <build> 5 <plugins> 6 <plugin> 7 <groupId>org.graalvm.nativeimage</groupId> 8 <artifactId>native-image-maven-plugin</artifactId> 9 <version>20.1.0</version> 10 <configuration> 11 <buildArgs>-J-Xmx4G -H:+TraceClassInitialization -H:+ReportExceptionStackTraces 12 -Dspring.graal.remove-unused-autoconfig=true -Dspring.graal.remove-yaml-support=true 13 </buildArgs> 14 <imageName>${project.artifactId}</imageName> 15 </configuration> 16 <executions> 17 <execution> 18 <goals> 19 <goal>native-image</goal> 20 </goals> 21 <phase>package</phase> 22 </execution> 23 </executions> 24 </plugin> 25 <plugin> 26 <groupId>org.springframework.boot</groupId> 27 <artifactId>spring-boot-maven-plugin</artifactId> 28 </plugin> 29 </plugins> 30 </build> 31 </profile> 32</profiles>
The buildArgs
tag is crucial here! We need to configure everything needed to successfully run a native-image
command for our Spring Boot app as already used inside our compile.sh . Also the spring-boot-maven-plugin
is needed inside the Maven native profile again, since the native-image-maven-plugin
needs it there in order to work properly.
We can leave out the -cp $CP $MAINCLASS
parameter as it is already provided when using Maven. Adding ${project.artifactId}
is also a good idea in order to use our artifactId
as the name for the resulting executable. Otherwise we end up with a fully qualified class name like io.jonashackt.springbootgraal.springboothelloapplication
.
As already used inside the compile.sh
script, we need to have the start-class
property in place also:
1<properties> 2 <start-class>io.jonashackt.springbootgraal.SpringBootHelloApplication</start-class> 3... 4</properties>
This might be everything we need to do. But wait! I ran into this error…
Preventing ‘No default constructor found Failed to instantiate java.lang.NoSuchMethodException’ errors
Running the Maven build using the new profile with mvn -Pnative clean package
successfully compiled my Spring Boot app. But when I tried to run it, the app didn’t start up properly and crashed with the following error:
1./target/spring-boot-graal 2 3 . ____ _ __ _ _ 4 /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 5( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 6 \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 7 ' |____| .__|_| |_|_| |_\__, | / / / / 8 =========|_|==============|___/=/_/_/_/ 9 :: Spring Boot :: 10 11Jun 05, 2020 10:46:27 AM org.springframework.boot.StartupInfoLogger logStarting 12INFO: Starting application on PikeBook.fritz.box with PID 33047 (started by jonashecht in /Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target) 13Jun 05, 2020 10:46:27 AM org.springframework.boot.SpringApplication logStartupProfileInfo 14INFO: No active profile set, falling back to default profiles: default 15Jun 05, 2020 10:46:27 AM org.springframework.context.support.AbstractApplicationContext refresh 16WARNING: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springBootHelloApplication': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.jonashackt.springbootgraal.SpringBootHelloApplication]: No default constructor found; nested exception is java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>() 17Jun 05, 2020 10:46:27 AM org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener logMessage 18INFO: 19 20Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 21Jun 05, 2020 10:46:27 AM org.springframework.boot.SpringApplication reportFailure 22SEVERE: Application run failed 23org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springBootHelloApplication': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.jonashackt.springbootgraal.SpringBootHelloApplication]: No default constructor found; nested exception is java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>() 24 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1320) 25 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1214) 26 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557) 27 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) 28 at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) 29 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) 30 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) 31 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) 32 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:895) 33 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) 34 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) 35 at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:62) 36 at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758) 37 at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750) 38 at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) 39 at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) 40 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) 41 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) 42 at io.jonashackt.springbootgraal.SpringBootHelloApplication.main(SpringBootHelloApplication.java:10) 43Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.jonashackt.springbootgraal.SpringBootHelloApplication]: No default constructor found; nested exception is java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>() 44 at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:83) 45 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1312) 46 ... 18 more 47Caused by: java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>() 48 at java.lang.Class.getConstructor0(DynamicHub.java:3349) 49 at java.lang.Class.getDeclaredConstructor(DynamicHub.java:2553) 50 at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:78) 51 ... 19 more
I had a hard time figuring this one out! Especially since there was absolutely no difference between the way our compile.sh works compared to the native-image-maven-plugin
. The parameters are the same! But finally I found a difference – it’s all about the Spring Feature computed spring.components
(and yes, I know the docs told me so 🙂 )!
Running our compile.sh
script the Spring Feature computed a spring.components
file on the fly containing the 3 classes of our example project that are annotated with a typical Spring @Component
:
1$ ./compile.sh 2... 3Excluding 104 auto-configurations from spring.factories file 4Found no META-INF/spring.components -> synthesizing one... 5Computed spring.components is 6vvv 7io.jonashackt.springbootgraal.HelloRouter=org.springframework.stereotype.Component 8io.jonashackt.springbootgraal.HelloHandler=org.springframework.stereotype.Component 9io.jonashackt.springbootgraal.SpringBootHelloApplication=org.springframework.stereotype.Component 10^^^ 11Registered 3 entries 12Configuring initialization time for specific types and packages: 13#69 buildtime-init-classes #21 buildtime-init-packages #28 runtime-init-classes #0 runtime-init-packages
Using the native-image-maven-plugin
, the compilation process didn’t successfully compute a spring.components
file and thus doesn’t recognize the three annotated classes:
1$ mvn -Pnative clean package 2... 3Excluding 104 auto-configurations from spring.factories file 4Found no META-INF/spring.components -> synthesizing one... 5Computed spring.components is 6vvv 7^^^ 8Registered 0 entries 9Configuring initialization time for specific types and packages: 10#69 buildtime-init-classes #21 buildtime-init-packages #28 runtime-init-classes #0 runtime-init-packages
spring-context-indexer to the rescue!
But why do we need all those classes inside a spring.components
file? That’s because we’re compiling a GraalVM Native Image from our Spring Boot app that runs on the SubstrateVM, which has a quite reduced feature set. And using dynamic component scanning at runtime isn’t supported with using native images!
The solution to this problem would be something to do the component scanning at build time! The one utility that has done this already for quite a while is the spring-context-indexer . Using the native-image-maven-plugin
we have to explicitly include the spring-context-indexer dependency inside our pom.xml :
1<dependency> 2 <groupId>org.springframework</groupId> 3 <artifactId>spring-context-indexer</artifactId> 4 </dependency>
Now running a Maven build, the file target/classes/META_INF/spring.components
containing our 3 needed classes is created:
1io.jonashackt.springbootgraal.HelloHandler=org.springframework.stereotype.Component 2io.jonashackt.springbootgraal.HelloRouter=org.springframework.stereotype.Component 3io.jonashackt.springbootgraal.SpringBootHelloApplication=org.springframework.stereotype.Component
Finally our Maven build works as expected and executes the native image compilation like a charm! Simply run the build with:
1$ mvn -Pnative clean package
For a full example of a Spring Boot GraalVM native image compilation with Maven, check out this TravisCI build .
Using the native-image-maven-plugin with Docker
As we already learned in the last post about Running Spring Boot GraalVM Native Images with Docker & Heroku , using Docker to compile our Spring Boot native images makes for a great combination. If you followed all the steps in the current post and extended your pom.xml
with the native
profile, using the native-image-maven-plugin
with Docker should be easy. Let’s look at the Dockerfile
:
1# Simple Dockerfile adding Maven and GraalVM Native Image compiler to the standard 2# https://hub.docker.com/r/oracle/graalvm-ce image 3FROM oracle/graalvm-ce:20.1.0-java11 4 5ADD . /build 6WORKDIR /build 7 8# For SDKMAN to work we need unzip & zip 9RUN yum install -y unzip zip 10 11RUN \ 12 # Install SDKMAN 13 curl -s "https://get.sdkman.io" | bash; \ 14 source "$HOME/.sdkman/bin/sdkman-init.sh"; \ 15 sdk install maven; \ 16 # Install GraalVM Native Image 17 gu install native-image; 18 19RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && mvn --version 20 21RUN native-image --version 22 23RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && mvn -Pnative clean package 24 25 26# We use a Docker multi-stage build here in order to only take the compiled native Spring Boot App from the first build container 27FROM oraclelinux:7-slim 28 29MAINTAINER Jonas Hecht 30 31# Add Spring Boot Native app spring-boot-graal to Container 32COPY --from=0 "/build/target/spring-boot-graal" spring-boot-graal 33 34# Fire up our Spring Boot Native app by default 35CMD [ "sh", "-c", "./spring-boot-graal -Dserver.port=$PORT" ]
We didn’t need to change much here – we only need to use our Maven command mvn -Pnative clean package
instead of our compile.sh
here. Additionally the GraalVM base image is also updated to oracle/graalvm-ce:20.1.0-java11
. If you followed this blog series’ posts, you also need to change the location from where the native image is copied from the first build container in this multi-stage Docker build. Since we’re using the Maven plugin, the resulting spring-boot-graal
simply resides in /build/target/
.
Logo sources: Docker logo , Spring Boot logo , GraalVM logo , Maven logo
Run the Docker build with docker build . --tag=spring-boot-graal
and then later start the natively compiled Spring Boot app inside a container via:
1docker run -p 8080:8080 spring-boot-graal
Using the native-image-maven-plugin to compile our Spring Boot GraalVM native images is fun!
Trying to use a techology which is currently under heavy development like the Spring Boot GraalVM Native Image support sometimes has its challenges. Using a bash script here to get a more profound understandig of what’s happening behind the scenes absolutely makes sense. Especially if we need to craft a working native-image
command for the compilation!
But as already stated, the Spring team is really doing a great job – and the required configuration is getting simpler with every release of the Spring experimental project spring-graalvm-native . Heading to a more stable release, it’s for sure a good idea to start using the native-image-maven-plugin , as we’re already used to, while using other GraalVM-based frameworks like Quarkus.io . And as my former colleague Benedikt Ritter rightly said , we should use a more modern way than bash scripts in order to build our apps today. 🙂
More articles
fromJonas Hecht
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Jonas Hecht
Senior Solution Architect
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.