Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
authors:
- meri
tags:
- hyperfocal
---

# Armeria Hyperfocal #1: Building a Native Image with Armeria and GraalVM

Learn how to build a GraalVM native image for an Armeria server—from a simple Hello World to annotated services.

{/* truncate */}

> **Armeria Hyperfocal** — where everything comes into sharp focus.

Welcome to the very first post of **Armeria Hyperfocal**! In this series, we’re diving into a high-performance topic: **GraalVM Native Image integration**. If you've ever wanted your Java services to start instantly and consume a fraction of the usual memory, this guide is for you.

## 🎯 Topic Overview

In the world of cloud-native microservices, **startup time** and **memory footprint** are critical. Traditional JVM applications can be heavy and slow to warm up.

In this post, we’ll explore how Armeria’s native support for GraalVM allows you to transform your Java bytecode into a standalone executable. We’ll start with a simple "Hello World" and move on to more complex **Annotated Services**, explaining how to handle the common pitfalls of reflection along the way.

Quick start: [https://github.com/line/armeria-hyperfocal/tree/main/01/native-image](https://github.com/line/armeria-hyperfocal/tree/main/01/native-image)

## 💡 Walkthrough

### 1. Setting up the Environment

To get started, we recommend using a Gradle-based project. Since GraalVM requires a specific environment, we use the `foojay.io` toolchain resolver in `settings.gradle.kts` to automatically handle the GraalVM download.

```java
rootProject.name = "native-image"

plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.6.0"
}
```

In your `build.gradle.kts`, we use a specialized plugin from **Palantir** to streamline the native image build process.

```java
plugins {
id 'com.palantir.graal' version '0.12.0'
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
vendor = JvmVendorSpec.matching("GraalVM Community")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood that we assumed users would configure Armeria dependencies themselves.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed that since we provide the whole demo code here: https://github.com/line/armeria-hyperfocal/blob/main/01/native-image/build.gradle.kts

nativeImage {
mainClass = 'com.linecorp.armeria.examples.Main'
executableName = 'armeria-native-image'
// Optional: Add specific GraalVM flags here
}
```
Comment on lines +40 to +59
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For the Palantir Graal Gradle plugin, what is the correct Kotlin DSL (build.gradle.kts`) syntax for:

  1. plugin declaration
  2. setting mainClass and executableName in nativeImage?
    Please cite official plugin docs/examples.`

💡 Result:

  1. Plugin declaration in Kotlin DSL: plugins { id("com.palantir.graal") version "0.13.0" } 2) Setting mainClass and executableName in nativeImage: graal { mainClass = "com.example.Main" outputName = "my-executable" } Note: The plugin creates a 'graal' extension block for configuration. The executableName corresponds to 'outputName' as per official README. No official Kotlin DSL examples found in repo tests (which use Groovy), but standard Gradle Kotlin DSL syntax applies to property setters. The plugin is unmaintained per GitHub warning, latest release March 2026.

Citations:


build.gradle.kts example has three critical issues: Groovy syntax, incorrect configuration block name, and wrong property name.

The snippet uses Groovy syntax (single quotes, id 'com.palantir.graal') which is invalid for Kotlin DSL. Additionally, the configuration block should be graal not nativeImage, and the executable property should be outputName not executableName per the official Palantir plugin documentation.

Corrected Kotlin DSL snippet
 plugins {
-    id 'com.palantir.graal' version '0.12.0'
+    id("com.palantir.graal") version "0.12.0"
 }
 
 java {
     toolchain {
         languageVersion = JavaLanguageVersion.of(17)
         vendor = JvmVendorSpec.matching("GraalVM Community")
     }
 }
 
-nativeImage {
-    mainClass = 'com.linecorp.armeria.examples.Main'
-    executableName = 'armeria-native-image'
+graal {
+    mainClass = "com.linecorp.armeria.examples.Main"
+    outputName = "armeria-native-image"
     // Optional: Add specific GraalVM flags here
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
In your `build.gradle.kts`, we use a specialized plugin from **Palantir** to streamline the native image build process.
```java
plugins {
id 'com.palantir.graal' version '0.12.0'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
vendor = JvmVendorSpec.matching("GraalVM Community")
}
}
nativeImage {
mainClass = 'com.linecorp.armeria.examples.Main'
executableName = 'armeria-native-image'
// Optional: Add specific GraalVM flags here
}
```
In your `build.gradle.kts`, we use a specialized plugin from **Palantir** to streamline the native image build process.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@site-new/src/content/blog/en/2026-04-10-armeria-hyperfocal-1-building-a-native-image-with-armeria-and-graalvm.mdx`
around lines 40 - 59, The snippet uses Groovy DSL and wrong plugin
block/property names; convert to Kotlin DSL by using plugins {
id("com.palantir.graal") version "0.12.0" } and Kotlin string syntax (double
quotes), keep the java.toolchain as-is, then replace the wrong nativeImage block
with the Palantir plugin's graal block and use outputName instead of
executableName and set mainClass with a Kotlin string (e.g., graal { mainClass =
"com.linecorp.armeria.examples.Main"; outputName = "armeria-native-image" }).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually does look like groovy,

perhaps either migrate documentation to build.gradle, settings.gradle,
or modify to

plugins {
    id("com.palantir.graal") version "0.12.0"
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
        vendor.set(JvmVendorSpec.matching("GraalVM Community"))
    }
}

nativeImage {
    mainClass("com.linecorp.armeria.examples.Main")
    executableName("armeria-native-image")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@jrhee17, great catch — the code is indeed Groovy syntax and needs to be updated. However, there's a small correction to your suggested Kotlin DSL: the configuration block for the Palantir com.palantir.graal plugin is graal { ... }, not nativeImage { ... } (which is just the name of the Gradle task, not the extension). Additionally, the property for the executable name is outputName, not executableName.

The ./gradlew nativeImage task creates the native image, but the plugin is configured through the graal Gradle extension block. Kotlin DSL examples for this plugin show graal { mainClass("...") outputName("...") } method-call style.

The corrected Kotlin DSL for build.gradle.kts should be:

plugins {
    id("com.palantir.graal") version "0.12.0"
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
        vendor.set(JvmVendorSpec.matching("GraalVM Community"))
    }
}

graal {
    mainClass("com.linecorp.armeria.examples.Main")
    outputName("armeria-native-image")
    // Optional: Add specific GraalVM flags here
}

Alternatively, if the author prefers Groovy DSL, migrating the examples to plain build.gradle/settings.gradle would also be a clean solution — both options resolve the syntax issue.


🌐 Web query:

palantir gradle-graal plugin nativeImage graal block Kotlin DSL mainClass executableName

💡 Result:

With the Palantir gradle-graal plugin (com.palantir.graal), the native-image entry point and produced binary name are configured on the graal extension as:

  • mainClass(...) (required for nativeImage)
  • outputName(...) (this is the “executableName”; don’t pass -H:Name=... via option(...)) (github.com)

Gradle Kotlin DSL (build.gradle.kts)

import com.palantir.gradle.graal.GraalExtension

plugins {
  id("com.palantir.graal") version "0.13.0"
}

extensions.configure<GraalExtension>("graal") {
  mainClass("com.example.MainKt")
  outputName("my-cli")

  // extra native-image args:
  option("--no-fallback")
  option("-H:+ReportExceptionStackTraces")
}

Then build with:

./gradlew nativeImage

The plugin writes outputs under build/graal/. (github.com)

Citations:


@jrhee17, great catch — the code is indeed Groovy syntax and needs to be updated. However, there's a small correction needed in your suggested Kotlin DSL snippet. The configuration block for the Palantir com.palantir.graal plugin is graal { ... }, not nativeImage { ... }nativeImage is only the name of the Gradle task used to trigger the build (./gradlew nativeImage). Additionally, the produced binary name is configured via outputName(...), not executableName.

The Kotlin DSL configuration looks like this:

import com.palantir.gradle.graal.GraalExtension

plugins {
    id("com.palantir.graal") version "0.12.0"
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
        vendor.set(JvmVendorSpec.matching("GraalVM Community"))
    }
}

extensions.configure<GraalExtension>("graal") {
    mainClass("com.linecorp.armeria.examples.Main")
    outputName("armeria-native-image")
    // Optional: Add specific GraalVM flags here
}

Alternatively, if the author prefers Groovy DSL, migrating the examples to plain build.gradle/settings.gradle would also cleanly resolve the syntax issue — either option works.


### 2. A Simple Armeria Server

Before we dive into complex features, let’s see how a basic server is built. Even a simple "Hello World" server can be turned into a native binary.

```java
public final class Main{

public static void main(String[] args) throws Exception {
final Server server = Server
.builder()
.http(8080)
.https(8443)
.tlsSelfSigned()
.service("/", (ctx, req) -> HttpResponse.of("Hello, world!"))
.build();

server.start().join();
}
}
```

To trigger the native compilation, run:

```bash
$ ./gradlew nativeImage
```

**💡 A Pro-tip for the Build Process:**

When you run this command, GraalVM performs a deep static analysis of your code. You might see a lot of warnings flashing by in your terminal during this stage. Don't panic! This is perfectly normal. GraalVM is simply reporting on code paths it can't fully "see" through. As long as the build finishes and your binary is generated, you can safely ignore these warnings.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In GraalVM Native Image docs, are build-time warnings generally safe to ignore, or do docs recommend investigating specific categories (reflection/dynamic proxies/resources/JNI)?

💡 Result:

GraalVM Native Image docs do not explicitly state that build-time warnings are generally safe to ignore. Instead, they recommend investigating and configuring specific categories like reflection, dynamic proxies, resources, and JNI, as these often indicate missing reachability metadata required for runtime behavior. Official docs emphasize that Native Image performs static analysis but cannot always detect dynamic features (e.g., Reflection API calls, Proxy.newProxyInstance, JNI lookups, resource loading). When analysis fails, manual configuration files (reflect-config.json, proxy-config.json, jni-config.json, resource-config.json) or reachability-metadata.json must be provided during build. Build-time warnings, such as "WARNING: Could not register reflection metadata" or similar for proxies/resources/JNI, signal potential missing registrations that could cause runtime errors like MissingReflectionRegistrationError. To diagnose, use options like --exact-reachability-metadata, -H:ThrowMissingRegistrationErrors, or -XX:MissingRegistrationReportingMode=Warn. Build reports (--emit=build-report) and output breakdowns help identify issues. Examples from Spring Native/Quarkus show warnings for unresolvable reflection entries, resolved by refining hints/metadata rather than ignoring. While some warnings (e.g., deprecated options, module exports) may be harmless, those tied to dynamic features require investigation to ensure closed-world assumption holds and avoid runtime failures.

Citations:


Revise warning guidance to recommend investigating warnings rather than ignoring them.

GraalVM Native Image documentation explicitly recommends investigating build-time warnings—particularly those related to reflection, dynamic proxies, resources, and JNI—as they typically indicate missing reachability metadata. Warnings can signal missing registrations that cause runtime failures (e.g., MissingReflectionRegistrationError). Suggest updating the sentence to recommend reviewing warnings and configuring necessary metadata via reflect-config.json, proxy-config.json, jni-config.json, or resource-config.json rather than dismissing them as safe to ignore.

🧰 Tools
🪛 LanguageTool

[style] ~90-~90: Consider using a synonym to be more concise.
Context: ...ic analysis of your code. You might see a lot of warnings flashing by in your terminal d...

(A_LOT_OF)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@site-new/src/content/blog/en/2026-04-10-armeria-hyperfocal-1-building-a-native-image-with-armeria-and-graalvm.mdx`
at line 90, The current guidance tells readers to "safely ignore" GraalVM Native
Image build warnings; update the sentence to instead advise reviewing these
warnings (especially those mentioning reflection, dynamic proxies, resources,
and JNI) and taking corrective action by generating or adding the appropriate
reachability metadata (e.g., reflect-config.json, proxy-config.json,
jni-config.json, resource-config.json) or following GraalVM docs to avoid
runtime errors like MissingReflectionRegistrationError; modify the paragraph
around the GraalVM build warnings to recommend investigation and configuration
rather than dismissal.


### 3. Crafting a Server with Annotated Services

Now, let's try something more "real-world." Armeria’s `AnnotatedService` allows you to define paths using annotations like `@Get` and `@Param`.

```java
public final class Main{

public static void main(String[] args) throws Exception {
final Server server = Server
.builder()
.http(8080)
.https(8443)
.tlsSelfSigned()
.annotatedService(new Object() {
@Get("/greet/:name")
public String greet(@Param("name") String name) {
return "Hello, " + name + '!';
}
})
.service("/", (ctx, req) -> HttpResponse.of("Hello, world!"))
.build();

server.start().join();
}
}

// A simple annotated service
public class MyService {
@Get("/hello/{name}")
public String hello(@Param("name") String name) {
return "Hello, " + name + "!";
}
}
```

**The Catch: Reflection.** This is where it gets interesting. Armeria uses **Java Reflection** to scan these `@Get` and `@Param` annotations at runtime. However, GraalVM’s "tree-shaking" process removes reflection metadata by default to keep the binary small.

If you run an annotated service without extra configuration, it will fail because the server can't "see" your methods. To fix this, we use the **`native-image-agent`**.

1. Run with Agent: Run your JAR once with the agent attached. It watches the JVM and records every reflection call.

```bash
# Run with the agent to record reflection usage
java -agentlib:native-image-agent=config-output-dir=./src/main/resources/META-INF/native-image -jar my-app.jar
```

2. **Include Metadata:** The agent generates a `reflect-config.json`. Place this in `META-INF/native-image/` so GraalVM knows exactly which reflection data to keep during the build.

### 4. The Result: Performance Boost

Once the binary—`armeria-native-image`—is generated, the results are staggering:

| Metric | Standard JVM | Native Image |
| ------------------ | ------------ | ------------ |
| Startup Time | ~1–2 seconds | < 100ms |
| Memory Usage (RSS) | ~250MB+ | ~88MB |

As you can see, the Native Image starts nearly instantaneously and consumes a fraction of the memory because there is no heavy JVM overhead inside.

![](../../../../static/img/hyperfocal1-1.png)

![](../../../../static/img/hyperfocal1-2.png)

![](../../../../static/img/hyperfocal1-3.png)

![](../../../../static/img/hyperfocal1-4.png)
Comment on lines +151 to +157
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add meaningful alt text for screenshots.

All four images use empty alt text (![](...)), which reduces accessibility and context for non-visual readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@site-new/src/content/blog/en/2026-04-10-armeria-hyperfocal-1-building-a-native-image-with-armeria-and-graalvm.mdx`
around lines 151 - 157, The four Markdown image tags use empty alt text which
harms accessibility; update each tag by replacing the empty alt with meaningful
descriptive alt strings that reflect the screenshot content (e.g., "Armeria
project structure screenshot", "GraalVM native-image build output", "Server
running with native image", "Benchmark results or logs") referencing the
existing image filenames hyperfocal1-1.png, hyperfocal1-2.png, hyperfocal1-3.png
and hyperfocal1-4.png so reviewers can locate and verify edits (replace ![](...)
with e.g. ![Armeria project structure](...), etc.).


## 🧩 Key Takeaways

- **Cloud-Native Ready:** Native images provide the "instant-on" capability required for scale-to-zero architectures.
- **AOT Compilation:** Shifting the heavy lifting to build time creates a highly optimized, standalone executable.
- **Don't Forget the Metadata:** While Armeria comes with built-in metadata support, your specific application logic (like Annotated Services) needs the `native-image-agent` to ensure everything works perfectly in the native binary.

## 🔭 What’s Next

That’s it for this **Hyperfocal**! We hope this guide helps you deploy Armeria services more efficiently. You can find the full source code for this demo in our [GitHub repository](https://github.com/line/armeria-hyperfocal/tree/main/01/native-image).

In the next post, we’ll look at **Log Masking**—showing you how to keep sensitive data out of your logs with ease.

Stay tuned, and keep your focus sharp!
Binary file added site-new/static/img/hyperfocal1-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site-new/static/img/hyperfocal1-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site-new/static/img/hyperfocal1-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site-new/static/img/hyperfocal1-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading