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

No Comments

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

<dependencies>
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-graalvm-native</artifactId>
        <version>0.7.1</version>
    </dependency>
...
</dependencies>
 
 
<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </repository>
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </pluginRepository>
</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:

FROM 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):

GRAALVM_VERSION=`native-image --version`
echo "[-->] Compiling Spring Boot App '$ARTIFACT' with $GRAALVM_VERSION"
time native-image \
  -J-Xmx4G \
  -H:+TraceClassInitialization \
  -H:Name=$ARTIFACT \
  -H:+ReportExceptionStackTraces \
  -Dspring.graal.remove-unused-autoconfig=true \
  -Dspring.graal.remove-yaml-support=true \
  -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:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.nativeimage</groupId>
                    <artifactId>native-image-maven-plugin</artifactId>
                    <version>20.1.0</version>
                    <configuration>
                        <buildArgs>-J-Xmx4G -H:+TraceClassInitialization -H:+ReportExceptionStackTraces
                            -Dspring.graal.remove-unused-autoconfig=true -Dspring.graal.remove-yaml-support=true
                        </buildArgs>
                        <imageName>${project.artifactId}</imageName>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>native-image</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </profile>
</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:

<properties>
    <start-class>io.jonashackt.springbootgraal.SpringBootHelloApplication</start-class>
...
</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:

./target/spring-boot-graal
 
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::
 
Jun 05, 2020 10:46:27 AM org.springframework.boot.StartupInfoLogger logStarting
INFO: Starting application on PikeBook.fritz.box with PID 33047 (started by jonashecht in /Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target)
Jun 05, 2020 10:46:27 AM org.springframework.boot.SpringApplication logStartupProfileInfo
INFO: No active profile set, falling back to default profiles: default
Jun 05, 2020 10:46:27 AM org.springframework.context.support.AbstractApplicationContext refresh
WARNING: 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>()
Jun 05, 2020 10:46:27 AM org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener logMessage
INFO:
 
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
Jun 05, 2020 10:46:27 AM org.springframework.boot.SpringApplication reportFailure
SEVERE: Application run failed
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>()
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1320)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1214)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:895)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
	at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:62)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
	at io.jonashackt.springbootgraal.SpringBootHelloApplication.main(SpringBootHelloApplication.java:10)
Caused 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>()
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:83)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1312)
	... 18 more
Caused by: java.lang.NoSuchMethodException: io.jonashackt.springbootgraal.SpringBootHelloApplication.<init>()
	at java.lang.Class.getConstructor0(DynamicHub.java:3349)
	at java.lang.Class.getDeclaredConstructor(DynamicHub.java:2553)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:78)
	... 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:

$ ./compile.sh
...
Excluding 104 auto-configurations from spring.factories file
Found no META-INF/spring.components -> synthesizing one...
Computed spring.components is
vvv
io.jonashackt.springbootgraal.HelloRouter=org.springframework.stereotype.Component
io.jonashackt.springbootgraal.HelloHandler=org.springframework.stereotype.Component
io.jonashackt.springbootgraal.SpringBootHelloApplication=org.springframework.stereotype.Component
^^^
Registered 3 entries
Configuring initialization time for specific types and packages:
#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:

$ mvn -Pnative clean package
...
Excluding 104 auto-configurations from spring.factories file
Found no META-INF/spring.components -> synthesizing one...
Computed spring.components is
vvv
^^^
Registered 0 entries
Configuring initialization time for specific types and packages:
#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:

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
    </dependency>

Now running a Maven build, the file target/classes/META_INF/spring.components containing our 3 needed classes is created:

io.jonashackt.springbootgraal.HelloHandler=org.springframework.stereotype.Component
io.jonashackt.springbootgraal.HelloRouter=org.springframework.stereotype.Component
io.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:

$ 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:

# Simple Dockerfile adding Maven and GraalVM Native Image compiler to the standard
# https://hub.docker.com/r/oracle/graalvm-ce image
FROM oracle/graalvm-ce:20.1.0-java11
 
ADD . /build
WORKDIR /build
 
# For SDKMAN to work we need unzip & zip
RUN yum install -y unzip zip
 
RUN \
    # Install SDKMAN
    curl -s "https://get.sdkman.io" | bash; \
    source "$HOME/.sdkman/bin/sdkman-init.sh"; \
    sdk install maven; \
    # Install GraalVM Native Image
    gu install native-image;
 
RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && mvn --version
 
RUN native-image --version
 
RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && mvn -Pnative clean package
 
 
# We use a Docker multi-stage build here in order to only take the compiled native Spring Boot App from the first build container
FROM oraclelinux:7-slim
 
MAINTAINER Jonas Hecht
 
# Add Spring Boot Native app spring-boot-graal to Container
COPY --from=0 "/build/target/spring-boot-graal" spring-boot-graal
 
# Fire up our Spring Boot Native app by default
CMD [ "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/.

Spring Boot GraalVM Native Image compilation with the native-image-maven-plugin inside a Docker container

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:

docker 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. πŸ™‚

Jonas Hecht

Trying to bridge the gap between software architecture and hands on coding, Jonas hired at codecentric. He has deep knowledge in all kinds of enterprise software development, paired with passion for new technology. Connecting systems via integration frameworks Jonas learned to not only get the hang of technical challenges.

Comment

Your email address will not be published. Required fields are marked *