A 30MB native image with Helidon to run REST based microservices

You must have heard about the new kid on the block––Helidon from Oracle. It is an open source Java framework that enables one to write, among other things, lightweight microservices using functional and reactive programming paradigms. One can use Helidon’s simple yet powerful core reactive web server to quickly build cloud native microservices. And if you like writing applications using the 12 factor methodology, Helidon has you covered. Helidon’s config component provides multiple options to load and configure your app — from loading properties or YAML files to loading from external sources like Git. Out of the box metrics and tracing you ask…you got it!

In this article, I will take Helidon for a spin and build a sample CRUD microservice that will be exposed as a JSON REST API. We will then use jlink to create a custom native image that can run our microservice. Native image obviates the need for a full blown JRE. Trimming the size of the runtime and reducing the attack surface are important factors in cloud deployments. Of course, there are some cons to this approach but let’s table that for another day.

We will use JPMS and gradle to build our project. Here’s the relevant gradle build file to add Helidon libraries as our project dependencies:

plugins {
id "java"
id "com.zyxist.chainsaw" version "0.3.1"
}
sourceCompatibility = "10"
dependencies {
compile "io.helidon.webserver:helidon-webserver:0.9.1"
compile "io.helidon.webserver:helidon-webserver-netty:0.9.1"
compile "io.helidon.webserver:helidon-webserver-json:0.9.1"
}

You can pick and choose from the list of libraries provided by Helidon based on your application requirements. For our sample CRUD service, we just need the reactive web server powered by Netty and the JSON support. Since we are creating a modular jar using JPMS, let’s create a “module-info” class and specify the modules that we require for our application:

module example.api {
requires io.helidon.webserver;
requires io.helidon.webserver.json;
requires org.glassfish.java.json;
}

Note that though Helidon is fully modularized using JPMS, some of the underlying libraries like Netty are not. Such libraries rely on automatic module resolution.

With that setup, let’s create a Launcher class to host the main method:

import io.helidon.webserver.Http;
import io.helidon.webserver.Routing;
import io.helidon.webserver.WebServer;
...
public class Launcher {

public static void main(String[] args) {
WebServer
.create(createRouting())
.start()
.thenAccept(ws ->
logger.info("Service running at: http://localhost:" + ws.port()));
}

private static Routing createRouting() {
return Routing.builder()
// Add JSON support to all end-points
.register(JsonSupport.get())
.register("/api", new UserService())
// Global exception handler
.error(Exception.class, (req, res, ex) -> {
res.status(Http.Status.BAD_REQUEST_400).send();
})
.build();
}
}

That is all it takes to start a web server using Helidon. There’s no need for an application container, no wars to deploy, and no XML files to tweak.

Notice how we used a fluent routing builder class to register JSON support and add sub routes under the base path “/api”. In the Helidon world, sub routes can be created using “services.” They provide a means to organize your end-points and related logic. One can also use handlers to respond to incoming requests. You can read more about routing and the various APIs Helidon provides here.

To run this microservice, we simply need to run the main method. As you know, that can be done in multiple ways — by using an IDE for quick develop and test cycle, by using build tools like maven or gradle, or by manually packaging the jar and running the java command yourself. Once you start the microservice, you can use curl or your favorite REST client to do GET, POST, PUT and DELETE that were defined on our sample user service end-point. For example, if you do a GET for the first time, you will get an empty list:

curl -X GET http://localhost:8080/api/users/
{"items":[]}

As you can see, Helidon makes it really easy to develop REST APIs and microservices. And we have barely scratched the surface.

With this working prototype in hand, let’s build a custom JRE image that can run the prototype. JDK 9 introduced a tool called “jlink” that can be used to build custom native images. But there’s a catch — all the jars used by an application should be modularized using JPMS. They cannot rely on automatic module names. That is going to be a challenge in the real world. For example, as I mentioned before, Netty jars aren’t modularized in the JPMS sense. However, we can use the “jdeps” tool to figure out which modules from the JDK are being used by our application jars including their dependencies (= runtime jars), and based on that knowledge we can build a custom native JRE image.

Say you place all the runtime jars in the current working directory. You can run the jdeps command to print the summary of modules used by a specific jar:

jdeps --module-path . -s user-crud-microservice.jar

The output would be something like this:

example.user.api -> io.helidon.webserver
example.user.api -> io.helidon.webserver.json
example.user.api -> java.base
example.user.api -> java.logging
example.user.api -> org.glassfish.java.json

We can recursively execute jdeps for all our runtime jars and find the public modules of JDK which obviously start with “java.”. In fact, we can use unix tools like find, sed, sort and grep to semi-automate this process. You can refer to the full script used for our sample service here. Once we get the final list of Java modules, we can create a custom native image using this jlink command:

# Add jdk.unsupported to allow netty to access internal classes
jlink --module-path "${JAVA_HOME}/jmods" \
--add-modules jdk.unsupported"${JAVA_BASE_MODS}" \
--strip-debug \
--compress 2 \
--no-header-files \
--no-man-pages \
--output myimage

The size of the native image generated from the command above will be around 25 MB (slight variation is expected based on the platform). We can run our sample microservice using this native image. We need to pass in some additional options to the java command to let it know where to look for our runtime modules and to specify the main class:

myimage/bin/java --upgrade-module-path ${DEPLOY_DIR}/mods -m example.user.api/example.user.api.Launcher

The size of the runtime jars of our sample microservice will be around 5 MB. And so, with a grand total of 30 MB, we are able to create a self-contained native image that can run a REST based microservice using Helidon. Pretty cool!

Of course, executing several manual steps like we did is not a viable option for real world projects. I hope that the tooling around jdeps and jlink get better. Better yet, if all open source libraries provide modular versions of their jars, creating native images will be a breeze.

You can head over to Github to clone and play with the sample project. Happy Coding!