Beliebte Suchanfragen
//

Simplifying Spring Boot GraalVM Native Image builds with the native-image-maven-plugin

17.6.2020 | 10 minutes of reading time

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 from spring-graal-native to spring-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. 🙂

share post

//

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.