We compile the Spring Boot application into native using GraalVM

A translation of the article was prepared ahead of the start of the course "Developer on the Spring Framework" .



Hello Spring lovers! Welcome to the next release of Spring Tips. Today weโ€™ll talk about recently implemented support for compiling Spring Boot applications in GraalVM. We already talked about GraalVM and native applications in another Spring Tips release on the topic of Spring Fu.



Let's recall what GraalVM is. GraalVM is a replacement for the standard C1 compiler in OpenJDK. You can read more about using GraalVM in my Bootiful Podcast podcast with Chris Thalinge, a GraalVM contributor and Twitter engineer. Under certain conditions, GraalVM allows you to run regular Spring applications faster and, at least for this reason, it deserves attention.

But we will not talk about it. We will look at other GraalVM components: native image builder and SubstrateVM. SubstrateVM allows you to create native executables for your Java application. By the way, about this and other uses of GraalVM there was a podcast with Oleg Shelaev from Oracle Labs. Native image builder is a test for compromise. If you provide GraalVM with enough information about the behavior of your application in runtime (dynamically linked libraries, reflection, proxies, etc.), it will be able to turn your Java application into a statically linked binary, like an application in C or Golang. Honestly, this process can be quite painful. But if you do, you can generate native code that will be incredibly fast. As a result, the application will take up much less RAM and run in less than a second. Less than a second. Pretty tempting, isn't it? Of course!

However, remember that some points need to be taken into account. The resulting GraalVM binaries are not Java applications. They do not even run on a regular JVM. GraalVM is being developed by Oracle Labs and there is some kind of interaction between the Java and GraalVM teams, but I would not call it Java. The resulting binary will not be cross-platform. The application does not use the JVM. It runs in another runtime called SubstrateVM.

So there are a lot of trade-offs here, but nonetheless, I think GraalVM has great potential, especially for cloud-based applications where scaling and efficiency are paramount.

Let's start. Install GraalVM. You can download it here , or install using SDKManager. For installing Java distributions, I like to use SDKManager. GraalVM is slightly behind the latest versions of Java and currently supports Java 8 and 11. Support for Java 14 or 15 or later (which version will be there when you read this) is missing.

To install GraalVM for Java 8, run:

sdk install java 20.0.0.r8-grl

I recommend using Java 8 rather than Java 11, as there are some obscure errors in Java 11 that I haven't figured out yet.

After that, you need to install the native image builder component. Run:

gu install native-image
gu- this is a utility from GraalVM.

Lastly, check what JAVA_HOMEpoints to GraalVM. On my machine (Macintosh with SDKMAN) mine JAVA_HOMElooks like this:

export JAVA_HOME=$HOME/.sdkman/candidates/java/current/

Now that you have everything set up, let's take a look at our application. Go to Spring Initializr and generate a new project using Lombok, R2DBC, PostgreSQL and Reactive Web.

You saw a similar code a million times, so I wonโ€™t disassemble it, but just give it here.

package com.example.reactive;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.stream.Stream;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

@Log4j2
@SpringBootApplication(proxyBeanMethods = false)
public class ReactiveApplication {

    @Bean
    RouterFunction<ServerResponse> routes(ReservationRepository rr) {
        return route()
            .GET("/reservations", r -> ok().body(rr.findAll(), Reservation.class))
            .build();
    }

    @Bean
    ApplicationRunner runner(DatabaseClient databaseClient, ReservationRepository reservationRepository) {
        return args -> {

            Flux<Reservation> names = Flux
                .just("Andy", "Sebastien")
                .map(name -> new Reservation(null, name))
                .flatMap(reservationRepository::save);

            databaseClient
                .execute("create table reservation ( id   serial primary key, name varchar(255) not null )")
                .fetch()
                .rowsUpdated()
                .thenMany(names)
                .thenMany(reservationRepository.findAll())
                .subscribe(log::info);
        };
    }


    public static void main(String[] args) {
        SpringApplication.run(ReactiveApplication.class, args);
    }
}

interface ReservationRepository extends ReactiveCrudRepository<Reservation, Integer> {
}


@Data
@AllArgsConstructor
@NoArgsConstructor
class Reservation {

    @Id
    private Integer id;
    private String name;
}

You can see the full code here .

The only feature of this application is that we use the Spring Boot attribute proxyBeanMethodsto ensure that the application will not use cglib and other proxies other than the JDK. GraalVM does not support non-JDK proxies. Although even with the JDK proxy, you have to tinker with it so that GraalVM learns about them. This attribute, new to Spring Framework 5.2, is partly intended to support GraalVM.

So, let's move on. I mentioned earlier that we have to tell GraalVM about some points that may be in our application at runtime and that it may not understand when executing native code. These are things like reflection, proxies, etc. There are several ways to do this. You can describe the configuration manually and include it in the assembly. GraalVM will automatically pick it up. Another way is to run the program with a Java agent that monitors what the application is doing and, after the application finishes, writes everything to the configuration files, which can then be transferred to the GraalVM compiler.

You can also use the GraalVM feature. (Note Translator: โ€œfeatureโ€œ - the term GraalVM denotes a plug-in for native compilation, creating a native binary from a class file ) . The GraalVM feature is similar to a Java agent. It can do some kind of analysis and pass information to the GraalVM compiler. Feature knows and understands how the Spring application works. She knows when Spring beans are proxies. She knows how to dynamically create classes in runtime. She knows how Spring works, and she knows what GraalVM wants, at least most of the time (after all, this is an early release!)

You also need to configure the build. Here is minepom.xml.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.M4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>reactive</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <start-class>
            com.example.reactive.ReactiveApplication
        </start-class>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-graal-native</artifactId>
            <version>0.6.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-indexer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>io.r2dbc</groupId>
            <artifactId>r2dbc-h2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <finalName>
            ${project.artifactId}
        </finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

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


    <profiles>
        <profile>
            <id>graal</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.nativeimage</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>20.0.0</version>
                        <configuration>
                            <buildArgs>
-Dspring.graal.mode=initialization-only -Dspring.graal.dump-config=/tmp/computed-reflect-config.json -Dspring.graal.verbose=true -Dspring.graal.skip-logback=true --initialize-at-run-time=org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils --initialize-at-build-time=io.r2dbc.spi.IsolationLevel,io.r2dbc.spi --initialize-at-build-time=io.r2dbc.spi.ConstantPool,io.r2dbc.spi.Assert,io.r2dbc.spi.ValidationDepth --initialize-at-build-time=org.springframework.data.r2dbc.connectionfactory -H:+TraceClassInitialization --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces --no-server --initialize-at-build-time=org.reactivestreams.Publisher --initialize-at-build-time=com.example.reactive.ReservationRepository --initialize-at-run-time=io.netty.channel.unix.Socket --initialize-at-run-time=io.netty.channel.unix.IovArray --initialize-at-run-time=io.netty.channel.epoll.EpollEventLoop --initialize-at-run-time=io.netty.channel.unix.Errors
                            </buildArgs>
                        </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>

</project>

Here we pay attention to the plugin native-image-maven-plugin. He takes parameters through the command line, which help him understand what needs to be done. All of these parameters are buildArgsnecessary for the application to start. (I have to express my deep gratitude to Andy Clement, Spring GraalVM Feature maintainer, for helping me figure out all of these options!)

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-graal-native</artifactId>
    <version>0.6.0.RELEASE</version>
</dependency>

We want the GraalVM compiler to have as many ways as possible to provide information on how the application should work: java agent, GraalVM Feature, command line parameters. All this together gives GraalVM enough information to successfully turn the application into a statically compiled native binary. In the long run, our goal is Spring projects. And the Spring GraalVM feature provides everything you need to support them.

Now that we have everything set up, let's put together an application:

  • Compile the Java application in the usual way
  • Launch a Java application with a Java agent to collect information. At this stage, we need to make sure that the application is working. You need to go through all the possible use cases. By the way, this is a very good case for using CI and tests! Everyone is constantly talking about testing the application and improving performance. Now, with GraalVM, you can do both!
  • Then rebuild the application, this time with the active graal profile, to compile it into the native application, using the information collected at the first start.

mvn -DskipTests=true clean package
export MI=src/main/resources/META-INF
mkdir -p $MI 
java -agentlib:native-image-agent=config-output-dir=${MI}/native-image -jar target/reactive.jar

## it's at this point that you need to exercise the application: http://localhost:8080/reservations 
## then hit CTRL + C to stop the running application.

tree $MI
mvn -Pgraal clean package

If everything went without errors, then in the directory targetyou will see a compiled application. Run it.

./target/com.example.reactive.reactiveapplication

The application starts, as can be seen from output similar to this.

2020-04-15 23:25:08.826  INFO 7692 --- [           main] c.example.reactive.ReactiveApplication   : Started ReactiveApplication in 0.099 seconds (JVM running for 0.103)

Not bad? GraalVM native image builder is great for paired with a cloud platform like CloudFoundry or Kubernetes. You can easily assemble the application into a container and run it in the cloud with minimal resources.
As always, we will be happy to hear from you. Is this technology right for you? Questions? Comments? Twitter (@springcentral) .



Learn more about the course

All Articles