1. Introduction

In this tutorial, we’ll explore the process of running a sample HTTP Scala application using GraalVM, building a native executable with the native-image tool, and Dockerizing the app.

2. GraalVM

GraalVM is an extension for the Java Virtual Machine (JVM) that provides support for multiple languages and execution modes, as well as increasing performance. It includes a high-performance Java compiler called Graal, which can operate in just-in-time (JIT) or ahead-of-time (AOT) configurations.

GraalVM aims to improve the performance of JVM-based languages and provide a virtual machine capable of running programs written in various languages, such as JavaScript, Python, Ruby, R, JVM-based languages (Java, Scala, Kotlin), and LLVM-based languages (C, C++).

Furthermore, GraalVM has a native-image tool that transforms Java applications into native binary executables using AOT compilation. Consequently, applications can run without a JVM, resulting in benefits such as reduced startup time, decreased memory consumption, more predictable performance, and lower CPU usage compared to a traditional JVM execution.

However, some limitations exist, such as the need to specify classes for dynamic loading at runtime due to reflection issues.

3. Installing GraalVM

First, we need to set up our environment and install GraalVM. The installation process depends on an OS, so we’ll highlight all OS-specific steps needed to set it up.

3.1. Downloading GraalVM

First, we need to download the GraalVM for a specific operating system from the official GitHub repo. For this tutorial, let’s pick Java 11 to have compatibility with other third-party libraries.

Next, we need to extract the package to some directory. It can be any directory, but let’s use common paths. For Windows, we can extract the package to C:\Program Files\graalvm. For Linux, we can go with /opt/graalvm, and for macOS, /usr/local/graalvm.

Besides, for UNIX-based systems, we can use third-party tools like sdkman to install GraalVM.

3.2. Adding GraalVM to the PATH Environment Variable

Next, in order to use GraalVM tools from the command line, we need to add the path to GraalVM binaries (bin directory inside the graalvm directory) to the system PATH variable. For Windows, we add this path to the PATH variable in the Environmental Variables settings. On the other hand, for Linux, we need to add a line such as:

export PATH=/opt/graalvm/bin:$PATH

to the ~/.bashrc or ~/.profile file, and for macOS:

export PATH=/usr/local/graalvm/bin:$PATH

should go to ~/.bashrc or ~/.zshrc file.

After updating the PATH environment variable, we may need to reboot the system or run source ~/.bashrc, source ~/.profile, or source ~/.zshrc (depending on shell configuration) in the terminal to apply the changes and make the GraalVM executables available in the command prompt or terminal.

Finally, let’s verify that we can execute GrallVM from the command line:

$ java –-version

We should see the output containing GraalVM:

openjdk 11.0.18 2023-01-17
OpenJDK Runtime Environment GraalVM CE 22.3.1 (build 11.0.18+10-jvmci-22.3-b13)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.1 (build 11.0.18+10-jvmci-22.3-b13, mixed mode, sharing)

It’s important to note that this will override the previously installed JDKs if the new path is added above the existing JDK paths. So, for some configurations, we can just use a full path for the Java GraalVM binary without adding it to the global PATH variable.

4. Creating a Scala HTTP Application

Now, we have everything set up, and it’s time to create a Scala application. We’re assuming that we already have Scala set up locally. For this tutorial, we’ll create a simple HTTP application using the Akka HTTP library.

4.1. Akka HTTP Application

Let’s create the main file of our application under the path src/main/scala/Main.scala:

object Main extends App {

  val host = "0.0.0.0"
  val port = 9000

  implicit val system: ActorSystem = ActorSystem(name = "baeldunghttpapp")
  implicit val materializer: ActorMaterializer = ActorMaterializer()

  import system.dispatcher

  import akka.http.scaladsl.server.Directives._

  def route = path("hello") {
    get {
      complete("Hello from Baeldung!")
    }
  }

  val binding = Http().bindAndHandle(route, host, port)
  binding.onComplete {
    case Success(_) => println("ScalaGraalVM: Listening for incoming connections!")
    case Failure(error) => println(s"Failed: ${error.getMessage}")
  }
  scala.io.StdIn.readLine()
}

This is a simple HTTP application with only one single route, /hello, which renders the plain text Hello from Baeldung!

To test the app, let’s run it using the sbt run command:

[info] running Main
ScalaGraalVM: Listening for incoming connections!

And if we go to http://localhost:9000 now, we should get the Hello from Baeldung! text.

4.2. Assembling the Scala App to a JAR

The native-image tool accepts a JAR file as input to build a native image, so we need to package our Scala app into that format. For that purpose, we’re going to use the sbt-assembly plugin.

Let’s add the following dependency code into the project/plugins.sbt file:

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5")

It’s important to note that we also need to provide a strategy for merging duplicated entries while packaging our JAR. To do that, let’s add a few lines to our build.sbt file so it contains:

name := """scala-graalvm-http-app"""
organization := "com.baeldung"

version := "1.0-SNAPSHOT"

scalaVersion := "2.13.10"
libraryDependencies ++= Seq("com.typesafe.akka" %% "akka-actor" % "2.8.0", "com.typesafe.akka" %% "akka-stream" % "2.8.0", "com.typesafe.akka" %% "akka-http" % "10.5.0")

assemblyMergeStrategy in assembly := {
  case "reference.conf" => MergeStrategy.concat
  case x =>
    val oldStrategy = (assemblyMergeStrategy in assembly).value
    oldStrategy(x)
}

Now, we’re able to run the command to get the packaged JAR file:

$ path/to/graalvm/java -jar .\target\scala-2.13\scala-graalvm-http-app-assembly-1.0-SNAPSHOT.jar

As a result, we should see the same output that we got from the sbt run command.

5. GraalVM Native Image

However, as we’ve already noted, the native-image tool has some limitations, especially for using reflection. As a result, when using reflection with native-image, we must inform the compiler about the classes that will be reflected upon, allowing it to generate the required data.

On the other hand, identifying the Java reflection classes used by the libraries in Scala applications can be challenging. Fortunately, GraalVM’s native-image-agent tool can simplify this process by tracking dynamic feature usage during execution on a regular Java VM. The tool runs alongside the java command and generates configuration files (.json files) for reflection, JNI, resource, and proxy usage.

But first, we need to install native-image tools:

$ gu install native-image

Now, we can continue with generating native image configuration files, especially the reflect-config.json for reflection support:

$ path/to/graalvm/java -agentlib:native-image-agent=config-
output-dir=.\src\main\resources\META-INF\native-image\com.baeldung\scala-graalvm-http-app -jar .\target\scala-2.13\scala-graalvm-http-app-assembly-1.0-SNAPSHOT.jar

In this command, we need to specify the META-INF directory as an output for our configuration files. We’re using the META-INF directory to package these configuration files later.

After completing this command, we should be able to find generated config JSON files. We can explore to see which classes and methods the application calls in runtime.

Additionally, native-image allows applications to be partially initialized at build time, reducing repetitive initialization code during startup. To explicitly define the classes we want to initialize at build time, we can use the –initialize-at-build-time flag in the native-image.properties file. Unfortunately, Scala 2.13 has some issues with using the Unsafe class that resides in scala.runtime.Statics that the native-image tool is not able to recognize.

So to tackle this issue, let’s create this file in the /src/main/resources/META-INF/native-image/org.scala-lang/scala-lang directory with the following content:

Args=--initialize-at-build-time=scala.runtime.Statics$VM

So, now let’s assemble our application again by running the same assembly command. As a result, we should be able to find our configuration files in the generated JAR archive.

Next, let’s build the native binary executable locally in the system:

$ native-image --static --verbose --allow-incomplete-classpath --report-unsupported-elements-at-runtime --no-fallback -jar target\scala-2.13\scala-graalvm-http-app-assembly-1.0-SNAPSHOT.jar

Of course, it takes some time to analyze and generate the executable. During the execution process, it shows which config files the native-image tool uses. As a result, we should get a native executable. Let’s run it:

$ scala-graalvm-http-app-assembly-1.0-SNAPSHOT.exe
ScalaGraalVM: Listening for incoming connections!

As we can see, it works the same way as our compiled JAR, so we can continue with Docker.

6. Dockerizing the Native Image Application

Finally, let’s Dockerize our app. First, we need to create a custom Dockerfile:

FROM findepi/graalvm:java11-all
RUN gu install native-image
WORKDIR /app
ADD target/scala-2.13/scala-graalvm-http-app-assembly-1.0-SNAPSHOT.jar httpgraalvmapp.jar
RUN native-image --static --verbose --allow-incomplete-classpath --report-unsupported-elements-at-runtime --no-fallback -jar httpgraalvmapp.jar httpgraalvmapp
ENTRYPOINT ["/app/httpgraalvmapp"]

Here, we’re using the base image of the same version we used for the GraalVM installation. Next, we add our assembled JAR file to the Docker image and run the same native-image command to build a native executable. And finally, we use our binary as an entry point to the image.

It’s time to build our image:

$ docker build ./ -t baeldung/scalagraalvmhttpapp 

When this command completes, we can use our message, so let’s run it:

$ docker run --name graalapp -p 9000:9000 baeldung/scalagraalvmhttpapp

Now, we can go to http://localhost:9000/hello and see our output.

7. Conclusion

In this tutorial, we’ve explored the process of running a Scala application using GraalVM as a native image inside Docker. By combining GraalVM and Docker, we can take advantage of the benefits offered by both technologies, such as improved performance and streamlined deployment.

As always, the source code for the examples is available over on GitHub.

2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.