Building a Vert.x Native Image

This howto shows all the current required steps and workarounds to build a vert.x native image with graalvm.

What you will build

  • You will write a multi-verticle application (client + server).

  • The code will cover security (SSL).

  • The code will be compiled to native with the help of GraalVM.

What you need

  • A text editor or IDE

  • GraalVM (20.3.0)

  • Maven

What is a native-image and why is Vert.x a good match?

A native-image is a optimized translation of a java application to OS/CPU specific native code. This new application will have different characteristics from a traditional JVM application, the most notable ones are:

  • faster startup times

  • (usually) smaller heap sizes

Native images are therefore very well suited for CLI or Serverless but nothing forbids its use also for server applications.

Native image compilation has several restrictions (by design) of which most do not apply to Vert.x core code. This makes vert.x a very good candidate to write native images. There are however, a couple of known limitations that can be easily worked around to build a native image.

This howto will cover these limitations and explain how to adapt a simple vert.x application to become native.

Create a project

Start by creating a new project that follows the following structure:

├── pom.xml
└── src
    └── main
        └── java
            └── vertx
                └── APIClientVerticle.java
                └── HTTPVerticle.java
                └── HTTPSVerticle.java

After that let’s walk over the important parts of the project. Here is the content of the pom.xml file that you should be using:

This is a minimal Hello World project. which you can confirm by reading what the code does:

package vertx;

import io.vertx.core.AbstractVerticle;

public class HTTPVerticle extends AbstractVerticle {

  @Override
  public void start() {
    vertx.createHttpServer().requestHandler(req -> {
      req.response()
        .putHeader("content-type", "text/plain")
        .end("Hello from Vert.x!");
    }).listen(8080, listen -> {
      if (listen.succeeded()) {
        System.out.println("Server listening on http://localhost:8080/");
      } else {
        listen.cause().printStackTrace();
        System.exit(1);
      }
    });
  }
}

Until this moment there is nothing new. It is a plain vert.x java project.

The same applies for the remaining sources (which will be covered later).

Add the GraalVM Native Image Maven Plugin

In order to build a native image we should add the native-image-maven-plugin

  1. We include the plugin which should match the installed graalvm version.

  2. Specifies the final name of the image (the executable name)

  3. The main start class

  4. The arguments to pass to the compiler (in this case we want to have some debug information in case things go wrong).

Building the image

Building the image is now as simple as:

mvn package

However this will fail as there is no public static void main method in the declared main class. We fix this by either adding it to HTTPVerticle:

  public static void main(String[] args) {
    Vertx.vertx().deployVerticle(new HTTPVerticle());
  }

Or configure the reflection config of graalvm to not exclude our verticles:

[
  {
    "name": "io.vertx.core.impl.launcher.commands.RunCommand",
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true
  },
  {
    "name": "io.vertx.core.impl.launcher.commands.VertxIsolatedDeployer",
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true
  },
  {
    "name": "java.lang.Long",
    "allDeclaredConstructors": true
  },
  {
    "name": "java.lang.Integer",
    "allDeclaredConstructors": true
  },

  {
    "name": "vertx.HTTPVerticle",
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true
  },
  {
    "name": "vertx.HTTPSVerticle",
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true
  },
  {
    "name": "vertx.APIClientVerticle",
    "allDeclaredConstructors": true,
    "allDeclaredMethods": true
  }
]

SSL

Adding support to SSL is not difficult but requires some updates. The reason is that security libraries will increase your final binary size considerably so all security features are disabled behind flags. There are also other caveats such as java keystores are allowed BUT must be in PKCS12 which is the new default format since Java9 but not on Java8 (which graalvm is based on).

You will now add an HTTPS vertcle server to your project, create the class HTTPSVerticle in the vertx package next to the existing one:

package vertx;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.JksOptions;

public class HTTPSVerticle extends AbstractVerticle {

  @Override
  public void start() {
    vertx.createHttpServer(
      new HttpServerOptions()
        .setSsl(true)             // (1)
        .setKeyStoreOptions(
          new JksOptions()
            .setPath("certificates.keystore")   // (2)
            .setPassword("localhost")           // (3)
        )
    ).requestHandler(req -> {
      req.response()
        .putHeader("content-type", "text/plain")
        .end("Hello from Vert.x!");
    }).listen(8443, listen -> {
      if (listen.succeeded()) {
        System.out.println("Server listening on https://localhost:8443/");
      } else {
        listen.cause().printStackTrace();
        System.exit(1);
      }
    });
  }
}
  1. Enable HTTPS for this server.

  2. Specify where to load the certificates from.

  3. What is the keystore password (you shouldn’t do this, you should get the password from a configuration, but for simplicity purposes it is hardcoded).

You need a certificate so this are the required steps:

# Generate the self signed test certificate
$ keytool
  -genkey \
  -alias vertx \
  -keypass localhost \
  -keystore certificates.keystore \
  -storepass localhost \
  -keyalg RSA

# Convert to PCKS12
$ keytool \
  -importkeystore \
  -srckeystore certificates.keystore \
  -destkeystore certificates.keystore \
  -deststoretype pkcs12

If you build the image this will compile, but won’t work at runtime. One thing is needed:

  1. Enable the security features to be added to the native image

To enable the security feature the graal plugin must be configured as:

<configuration>
  <imageName>${project.name}</imageName>
  <mainClass>${vertx.verticle}</mainClass>
  <buildArgs>--enable-all-security-services</buildArgs>
</configuration>

Native Clients

Now that we covered basic SSL security we can touch clients. For this example we will consume a simple HTTPS API.

package vertx;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;

public class APIClientVerticle extends AbstractVerticle {

  @Override
  public void start() {
    WebClient client = WebClient.create(vertx, new WebClientOptions().setSsl(true).setTrustAll(true));

    client
      .get(443, "icanhazdadjoke.com", "/")
      .putHeader("Accept", "text/plain")
      .send(ar -> {
        if (ar.succeeded()) {
          HttpResponse<Buffer> response = ar.result();
          System.out.println("Got HTTP response with status " + response.statusCode() + " with data "
              + response.body().toString("ISO-8859-1"));
        } else {
          ar.cause().printStackTrace();
        }
    });
  }
}

This client will make a HTTPS request and print the output on the console.

Build

At this moment you should have a basic application and your pom.xml should look like:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <name>hello_native</name>

  <properties>
    <vertx.verticle>vertx.APIClientVerticle</vertx.verticle>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.testSource>1.8</maven.compiler.testSource>
    <maven.compiler.testTarget>1.8</maven.compiler.testTarget>
    <graal.version>20.3.0</graal.version>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-stack-depchain</artifactId>
        <version>4.0.0-SNAPSHOT</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-core</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web-client</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>io.reactiverse</groupId>
        <artifactId>vertx-maven-plugin</artifactId>
        <version>1.0.22</version>
        <executions>
          <execution>
            <id>vmp</id>
            <goals>
              <goal>initialize</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <redeploy>true</redeploy>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.graalvm.nativeimage</groupId>
        <artifactId>native-image-maven-plugin</artifactId>
        <version>${graal.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>native-image</goal>
            </goals>
            <phase>package</phase>
          </execution>
        </executions>
        <configuration>
          <imageName>${project.name}</imageName>
          <mainClass>io.vertx.core.Launcher</mainClass>
          <buildArgs>-H:+PrintClassInitialization -H:+ReportExceptionStackTraces</buildArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Running mvn clean package will render a build failure. Currently and because vertx-core still depends on a netty release older than 4.1.53, we need to import the configuration added upstream manually. Which means we need to add a new file:

├── pom.xml
└── src
    └── main
        └── resources
            └── META-INF/native-image/com.example/myapp
                └── native-image.properties

This file are the configuration needed to be passed to the compiler. Currently the file is quite big but again once vertx core uses the latest netty many of these entries can be removed and only list the vert.x specific ones:

Args = --enable-http \
--enable-https \
--allow-incomplete-classpath \
--enable-all-security-services \
-H:EnableURLProtocols=http,https \
--report-unsupported-elements-at-runtime \
-H:ReflectionConfigurationResources=${.}/reflect-config.json \
-H:JNIConfigurationResources=${.}/jni-config.json \
-H:ResourceConfigurationResources=${.}/resource-config.json \
--initialize-at-build-time=org.slf4j.LoggerFactory,\
org.slf4j.impl.StaticLoggerBinder$,\
io.netty,\
io.vertx,\
com.fasterxml.jackson \
--initialize-at-run-time=io.netty.channel.DefaultChannelId,\
io.netty.buffer.PooledByteBufAllocator,\
io.vertx.core.net.impl.PartialPooledByteBufAllocator,\
io.netty.util.NetUtil,\
io.netty.channel.socket.InternetProtocolFamily,\
io.netty.resolver.HostsFileEntriesResolver,\
io.netty.resolver.dns.DnsNameResolver,\
io.netty.resolver.dns.DnsServerAddressStreamProviders,\
io.netty.resolver.dns.PreferredAddressTypeComparator\$1,\
io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider,\
io.vertx.core.impl.AddressResolver,\
io.vertx.core.dns.AddressResolverOptions,\
io.netty.handler.codec.http.websocketx.extensions.compression.DeflateEncoder,\
io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder,\
io.netty.handler.codec.http.HttpObjectEncoder,\
io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder,\
io.netty.handler.codec.http2.Http2CodecUtil,\
io.netty.handler.codec.http2.Http2ConnectionHandler,\
io.netty.handler.codec.http2.DefaultHttp2FrameWriter,\
io.netty.util.internal.logging.Log4JLogger,\
io.netty.handler.ssl.ReferenceCountedOpenSslServerContext,\
io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator,\
io.netty.handler.ssl.ReferenceCountedOpenSslEngine,\
io.netty.handler.ssl.ConscryptAlpnSslEngine,\
io.netty.handler.ssl.JettyNpnSslEngine,\
io.netty.handler.ssl.JettyAlpnSslEngine$ClientEngine,\
io.netty.handler.ssl.JettyAlpnSslEngine$ServerEngine,\
io.netty.handler.ssl.ReferenceCountedOpenSslContext,\
io.netty.handler.ssl.ReferenceCountedOpenSslClientContext,\
io.netty.buffer.ByteBufUtil$HexUtil,\
io.vertx.core.net.impl.transport.EpollTransport,\
io.vertx.core.net.impl.transport.KQueueTransport,\
io.vertx.core.http.impl.VertxHttp2ClientUpgradeCodec,\
io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder,\
io.vertx.core.eventbus.impl.clustered.ClusteredEventBus,\
io.netty.channel.unix,\
io.netty.channel.epoll,\
io.netty.buffer.ByteBufUtil$HexUtil

Running a compilation will be:

$ mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< com.example:myapp >--------------------------
[INFO] Building hello_native 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- vertx-maven-plugin:1.0.22:initialize (vmp) @ myapp ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ myapp ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 4 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ myapp ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to /home/paulo/Projects/vertx-howtos/graal-native-image-howto/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ myapp ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/paulo/Projects/vertx-howtos/graal-native-image-howto/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ myapp ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ myapp ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myapp ---
[INFO] Building jar: /home/paulo/Projects/vertx-howtos/graal-native-image-howto//target/myapp-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- native-image-maven-plugin:20.3.0:native-image (default) @ myapp ---
[INFO] ImageClasspath Entry: ...
[INFO] Executing: ...
[hello_native:59425]    classlist:   2,138.46 ms,  0.94 GB
[hello_native:59425]        (cap):     541.55 ms,  0.94 GB
[hello_native:59425]        setup:   1,947.31 ms,  0.94 GB
[hello_native:59425]     (clinit):   1,033.70 ms,  3.20 GB
[hello_native:59425]   (typeflow):  20,114.89 ms,  3.20 GB
[hello_native:59425]    (objects):  19,071.97 ms,  3.20 GB
[hello_native:59425]   (features):   1,535.82 ms,  3.20 GB
[hello_native:59425]     analysis:  43,375.18 ms,  3.20 GB
[hello_native:59425]     universe:   1,988.67 ms,  3.20 GB
[hello_native:59425]      (parse):   6,534.22 ms,  4.99 GB
[hello_native:59425]     (inline):  12,237.05 ms,  5.93 GB
[hello_native:59425]    (compile):  47,588.24 ms,  5.90 GB
[hello_native:59425]      compile:  69,333.28 ms,  5.90 GB
[hello_native:59425]        image:   4,784.16 ms,  5.90 GB
[hello_native:59425]        write:     819.88 ms,  5.90 GB
[hello_native:59425]      [total]: 124,619.09 ms,  5.90 GB
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:07 min
[INFO] Finished at: 2020-12-02T19:49:37+01:00
[INFO] ------------------------------------------------------------------------

Running the application will be as simple as:

$ ./target/hello_native run vertx.APIClientVerticle
Got HTTP response with status 200 with data Two peanuts were walking down the street. One was a salted.
^C

$ ./target/hello_native run vertx.HTTPServer
Dec 02, 2020 7:51:56 PM io.netty.channel.DefaultChannelId defaultProcessId
WARNING: Failed to find the current process ID from ''; using a random value: 1891971538
Dec 02, 2020 7:51:56 PM io.vertx.core.impl.launcher.commands.VertxIsolatedDeployer
INFO: Succeeded in deploying verticle
Server listening on http://localhost:8080/
^C

$ ./target/hello_native run vertx.HTTPSServer
Dec 02, 2020 7:56:25 PM io.netty.channel.DefaultChannelId defaultProcessId
WARNING: Failed to find the current process ID from ''; using a random value: -1125265761
Dec 02, 2020 7:56:25 PM io.vertx.core.impl.launcher.commands.VertxIsolatedDeployer
INFO: Succeeded in deploying verticle
Server listening on https://localhost:8443/
^C
Note
If you’re interested in knowing the RAM usage just execute: ps x -o pid,rss,command|grep hello_native and you will observe something like: 59751 35484 ./target/hello_native run vertx.HTTPVerticle About 36MB

You have now all the verticles in the same image. Remember that you can do now all the things you would with the launcher, for example scale the number of verticles for the HTTP server:

$ ./target/hello_native run vertx.HTTPVerticle -instances 4
Server listening on http://localhost:8080/
Server listening on http://localhost:8080/
Server listening on http://localhost:8080/
Server listening on http://localhost:8080/
^C

And this concludes the native image howto.

The full source of this howto can be found here

Summary

  • We wrote a HTTP server verticle.

  • We added the required metadata to build a image.

  • We wrote a HTTPS server verticle.

  • We added the required security dll and configuration to build a image.

  • We wrote a HTTPS client verticle.

  • We handled a build issue by specifying the order of load of classes.

  • We configured reflection and resources to have multiple verticles in a single image.


Last published: 2020-12-02 23:48:05 +0000.