Watch GraalVM Turn Your Java Into Binaries
There has been much buzz about GraalVM and what it means for the Java world. GraalVM is a Java distribution from Oracle that adds a bunch of features, most notably a new JIT compiler, polyglot capabilities, an LLVM runtime… and the ability to turn your Java application into a native binary.
This last one offers the potential to distribute Java applications as a single binary, and a few frameworks like Quarkus, Helidon, and Micronaut already take advantage of this feature. Native images also open up the possibility to distribute Java applications as CLI applications, which has recently been the near-exclusive domain of Go and Node. This tutorial will show you how!
Turn Your Java Application Into a Binary
GraalVM’s native-image
tool converts a Java application into a native binary. It isn’t just a repackaging of the application. The Java byte code compiles into native code ahead-of-time (AOT). This native code runs on a Substrate VM, a minimal virtual machine separate from the Java Virtual Machine. Without (just-in-time) JIT compilation, applications can start faster with less memory. As a side effect this simplifies running Java applications, removing the need for long shell scripts that resolve $JAVA_HOME
and setup the classpath.
In this post, I’ll walk through building a simple CLI that parses simple dice notation (e.g., “2d20”) and displays the result. I’ll first build the example with just Java, and then again with JavaScript to show of the polyglot features of GraalVM, building a single binary each time.
Install GraalVM
For this post, I’m going to use the Community Edition of GraalVM, as it is easy to install with SDKMAN. If you have SDKMAN installed you can run the command:
sdk install java 19.3.0.r11-grl
Next, install the native-image
tool using gu
(GraalVM Updater):
gu install native-image
You also need Apache Maven:
sdk install maven
Build a Simple Java Application
The type of application we build isn’t important as long as it can be run with native-image. I chose a simple dice parser instead of writing another “Hello World” application. You can grab the source from GitHub.
git clone https://github.com/oktadeveloper/okta-graalvm-example.git
cd okta-graalvm-example/jdk
This project contains a single Java class and is limited to parsing simple dice expressions like 2d20
(roll two different twenty-sided dice). Create a new java file src/main/java/com/okta/examples/jdk/JdkDiceApplication.java
:
package com.okta.examples.jdk;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
public class JdkDiceApplication {
public static void main(String[] args) {
if (args == null || args.length != 1) {
System.err.println("Usage: roll <dice_expression>");
System.err.println("Example: roll 2d20");
System.exit(1);
}
System.out.println(parseDiceNotation(args[0]));
}
private static int parseDiceNotation(String expression) {
// regex and match
String simpleDiceRegex = "(?<numberOfDice>\\d+)?[dD](?<numberOfFaces>\\d+)";
Matcher matcher = Pattern.compile(simpleDiceRegex).matcher(expression);
// fail if no match
if (!matcher.matches()) {
throw new IllegalStateException("Failed to parse dice expression: " + expression);
}
// default numberOfDice to 1
String numberOfDiceString = matcher.group("numberOfDice");
if (numberOfDiceString == null || numberOfDiceString.isEmpty()) {
numberOfDiceString = "1";
}
int numberOfDice = Integer.parseInt(numberOfDiceString);
int numberOfFaces = Integer.parseInt(matcher.group("numberOfFaces"));
// roll!
return IntStream.rangeClosed(1, numberOfDice)
.map(index -> new Random().nextInt(numberOfFaces) + 1)
.sum();
}
}
Next, we need a Maven pom.xml
file:
<?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.okta.examples</groupId>
<artifactId>okta-graal-example-jdk</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>19.3.0</version>
<configuration>
<mainClass>com.okta.examples.jdk.JdkDiceApplication</mainClass>
<imageName>roll</imageName>
</configuration>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
The native-image-maven-plugin
handles the creation of the native binary and specifies the following:
mainClass
- the application entry pointimageName
- the name of the binary build in thetarget/
directory)
Once you build the project with mvn package
, the resulting 6.6M binary can be found in the target directory target/roll
.
Roll some dice by running:
./target/roll 5d6
Exceptions are treated the same way as Java. We can force an exception by trying to parse an ill-formatted string:
./target/roll foobar
Exception in thread "main" java.lang.IllegalStateException: Failed to parse dice expression: foobar
at com.okta.examples.jdk.JdkDiceApplication.parseDiceNotation(JdkDiceApplication.java:28)
at com.okta.examples.jdk.JdkDiceApplication.main(JdkDiceApplication.java:17)
As expected the application exits with a status code of 1.
GraalVM’s Polyglot Support
One of the popular features of GraalVM is the ability to run other languages alongside your Java code. For example, you could use GraalVM to execute some JavaScript, R, or Python from your Java code. This is beneficial if you need to share code between different programming languages, or if you wanted to take advantage of running Node with a large heap and Java’s garbage collector.
I’ve put another example together in the javascript
directory of the example project.
GraalVM comes with a compatible version of Node.js that is optimized to run on the JVM. Confirm you are using the version of node
from the GraalVM distribution:
$JAVA_HOME/bin/node -e "console.log(process.version)" --show-version:graalvm
GraalVM Polyglot Engine Version 19.3.0
GraalVM Home /Users/bdemers/.sdkman/candidates/java/19.3.0.r11-grl
...
v12.10.0
NOTE: In my case, simply running
node
uses the version of Node.js managed bynvm
, you can confirm this by runningwhich node
. I’ll use$JAVA_HOME/bin/node
going forward.
Create a JavaScript Application
There are a few limitations on how you can execute JavaScript with GraalVM. If you run node
form the command line, most libraries should just work, and GraalVM tests over 100,000 packages from npmjs.org regularly. However, when you use the native-image
tool, things change. Notably, you cannot use module exports or use process
to get at the current environment.
To port the previous Java example to JavaScript, I’ve split the code up into two separate files to avoid the limitations mentioned above. Both files are located in src/main/resources/
so they can later be included in the native image, but more on that later.
First, create a dice-roller.js
with the string parsing logic:
function parseDiceNotation(expression) {
const simpleDiceRegex = /(?<numberOfDice>\d+)?[dD](?<numberOfFaces>\d+)/;
const matchObj = simpleDiceRegex.exec(expression);
if (!matchObj) {
throw "Failed to parse dice expression: "+ expression;
}
const numberOfDice = parseInt(matchObj.groups.numberOfDice) || 1;
const numberOfFaces = parseInt(matchObj.groups.numberOfFaces);
return Array.from({ length: numberOfDice },
(x, i) => { return Math.floor(Math.random() * numberOfFaces) + 1;})
.reduce((total, i) => { return total + i }, 0);
}
// module exports are not supported, use global scope
roll = parseDiceNotation;
Next create roll.js
, which runs the global roll
function when using the node
command:
'use strict';
require('./dice-roller.js');
const args = process.argv.slice(2);
if (!args || args.length !== 1) {
console.error("Usage: roll <dice_expression>");
console.error("Example: roll 2d20");
process.exit(1);
}
const expression = args[0];
console.log(roll(expression));
Execute roll.js
with GraalVM and flip a coin by running:
$JAVA_HOME/bin/node src/main/resources/roll.js 1d2
So far, this doesn’t look any different than using stock Node.js, but wait!
Mix JavaScript and Java
It’s been possible to execute JavaScript in a JVM forever, first with Rhino in Java 1.6 and then Nashorn in 1.8. However, these engines have been deprecated and GraalVM will likely replace them in the future.
When calling JavaScript from Java code, you can use the ScriptEngine
API from JSR 223, or classes from the org.graalvm.polyglot
package.
NOTE: GraalVM automatically provides the classes in
org.graalvm.polyglot
, so you do NOT need to add them as a dependency. However, I was unable to make IntelliJ’s code completion/compilation work with GraalVM 19.3.0 and had to revert to using the command line.
Create a class in src/main/java/com/okta/examples/javascript/JsDiceApplication.java
with a main
method that calls the JavaScript code in dice-roller.js
:
package com.okta.examples.javascript;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
public class JsDiceApplication {
public static void main(String[] args) throws Exception {
if (args == null || args.length != 1) {
System.err.println("Usage: roll <dice_expression>");
System.err.println("Example: roll 2d20");
System.exit(1);
}
System.out.println(parseDiceNotation(args[0]));
}
private static int parseDiceNotation(String expression) throws Exception {
// load the javascript file from the classpath
String diceRollerJs;
try (Scanner scanner = new Scanner(
JsDiceApplication.class.getResourceAsStream("/dice-roller.js"), StandardCharsets.UTF_8)) {
diceRollerJs = scanner.useDelimiter("\\A").next();
}
// parse the javascript file with a new context
Context context = Context.create("js");
context.eval("js", diceRollerJs);
// get a reference to the "roll" function
Value rollFunction = context.getBindings("js").getMember("roll");
// execute the function
return rollFunction.execute(expression).asInt();
}
}
We also need a Maven pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.okta.examples</groupId>
<artifactId>okta-graal-example-javascript</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<mainClass>com.okta.examples.javascript.JsDiceApplication</mainClass>
<arguments>2d20</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
Run the example with the Exec Maven Plugin: mvn compile exec:java
:
...
[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ okta-graal-example-javascript ---
30
...
Great! Now let’s build a native-image with this example.
Build a GraalVM Native Image with JavaScript Support
As with the first example, we can use the native-image-maven-plugin
to build the native image, add the following plugin block to your javascript/pom.xml
:
<plugin>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>19.3.0</version>
<configuration>
<mainClass>com.okta.examples.javascript.JsDiceApplication</mainClass>
<imageName>roll</imageName>
<buildArgs>--language:js</buildArgs>
</configuration>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
Take note of the buildArgs
parameter of --language:js
. Without this argument, JavaScript support is NOT included in the image. Including support for other languages also causes the build time to be significantly longer. ☕
Build the project with mvn package
and then run the new executable ./target/roll 10d10
.This results in the following error:
Exception in thread "main" java.lang.NullPointerException: source
at java.util.Objects.requireNonNull(Objects.java:246)
at java.util.Scanner.<init>(Scanner.java:595)
at com.okta.examples.javascript.JsDiceApplication.parseDiceNotation(JsDiceApplication.java:23)
at com.okta.examples.javascript.JsDiceApplication.main(JsDiceApplication.java:17)
This error has nothing to do with the inclusion of JavaScript. It is caused by the use of Class.getResourceAsStream
(the same is true for Class.getResource
). By default, resources are NOT included in the native image.
A Java Agent can help discover the needed resources for the image. The agent can be enabled with -agentlib:native-image-agent=config-merge-dir=<output-directory>
.
To include the agent, run:
java \
-agentlib:native-image-agent=config-merge-dir=./target/config-dir \
-cp target/okta-graal-example-javascript-1.0-SNAPSHOT.jar \
com.okta.examples.javascript.JsDiceApplication 10d10
This command is a bit long, but it is handy when troubleshooting larger applications. Take a look at resulting output in target/config-dir/resource-config.json
:
{
"resources":[
{"pattern":"META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider"},
{"pattern":"META-INF/services/com.oracle.truffle.api.instrumentation.TruffleInstrument$Provider"},
{"pattern":"META-INF/services/com.oracle.truffle.js.runtime.Evaluator"},
{"pattern":"META-INF/services/com.oracle.truffle.js.runtime.builtins.JSFunctionLookup"},
{"pattern":"META-INF/services/java.nio.file.spi.FileSystemProvider"},
{"pattern":"com/oracle/truffle/nfi/impl/NFILanguageImpl.class"},
{"pattern":"dice-roller.js"}
]
}
You can ignore all of the “truffle” resources; these are part of GraalVM (and explaining Truffle is outside the scope of this post.)
This example was a bit contrived, as it is obvious dice-roller.js
was the resource we needed to add.
Back in our pom.xml
update the buildArgs
line:
<buildArgs>--language:js -H:IncludeResources=dice-roller.js</buildArgs>
Rebuild the project with mvn package
, and run ./target/roll 10d10
to test the binary.
Success!
Are GraalVM Native Images Ready for Primetime?
GraalVM’s native image feature has me excited. Historically, distributing Java command-line tools has been a pain, we often resort to shipping zip files and shell scripts as a workaround. While GraalVM’s native-image
solves much of this pain, the project isn’t quite ready for everyone just yet. Substrate VM’s list of current limitations and the lack of Spring support might be the biggest drawbacks. GraalVM also lacks support for cross-compiling, which may add complexity to your CI pipeline to workaround. Binary size may also be an issue; the second example that embeds JavaScript caused the image size to ballon from 6.6M in the pure Java example to 92M.
Even with these limitations, GraalVM’s native-image can be used today for a wide variety of applications.
Learn more about GraalVM and Java Security
The source for this tutorial can be found on GitHub. If you are interested in learning more about using GraalVM and Java Security checkout the following links:
- How to Develop a Quarkus App with Java and OIDC Authentication
- 10 Excellent Ways to Secure Your Spring Boot Application
- GraalVM Documentation
If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, or subscribe to our YouTube channel!
Okta Developer Blog Comment Policy
We welcome relevant and respectful comments. Off-topic comments may be removed.