GraalVM Native Image¶
Hermes is fully compatible with GraalVM Native Image, enabling AOT compilation to native executables with zero configuration.
Overview¶
Hermes supports GraalVM native-image compilation out of the box:
- No reflection at runtime: Annotation processing happens at compile-time
- Zero configuration: Native-image metadata included in
hermes-core - ServiceLoader support: Logger provider discovery works in native images
- Fast startup: Sub-second application startup times
- Low memory footprint: Reduced memory usage compared to JVM
Prerequisites¶
- GraalVM 21.0+ or Liberica NIK 22+
- Java 17+
- Maven 3.8+ or Gradle 7+
Installation¶
Install GraalVM¶
Using SDKMAN¶
Manual Download¶
Download from GraalVM Downloads
Verify Installation¶
Maven Configuration¶
Using Native Maven Plugin¶
<project>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.28</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>my-app</imageName>
<mainClass>com.example.Application</mainClass>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
Build Native Image¶
Gradle Configuration¶
Using Native Gradle Plugin¶
plugins {
id("org.graalvm.buildtools.native") version "0.9.28"
}
graalvmNative {
binaries {
named("main") {
imageName.set("my-app")
mainClass.set("com.example.Application")
buildArgs.add("--no-fallback")
buildArgs.add("-H:+ReportExceptionStackTraces")
}
}
}
Build Native Image¶
Native Image Metadata¶
Hermes includes native-image metadata in hermes-core:
Location¶
hermes-core/src/main/resources/META-INF/native-image/io.github.dotbrains/hermes-core/
├── reflect-config.json
├── resource-config.json
├── jni-config.json
└── native-image.properties
reflect-config.json¶
[
{
"name": "io.github.dotbrains.core.HermesLoggerProvider",
"methods": [
{"name": "<init>", "parameterTypes": []}
]
},
{
"name": "io.github.dotbrains.core.HermesLogger",
"methods": [
{"name": "<init>", "parameterTypes": ["java.lang.String"]}
]
}
]
resource-config.json¶
{
"resources": {
"includes": [
{"pattern": "META-INF/services/.*"},
{"pattern": "hermes\\.properties"}
]
}
}
Spring Boot Native¶
Maven Configuration¶
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
</build>
Build Spring Boot Native Image¶
Run Native Container¶
Logging Configuration¶
application.properties¶
# Standard Hermes configuration works in native images
hermes.level.root=INFO
hermes.level.packages.com.example=DEBUG
hermes.pattern=%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
hermes.async.enabled=true
hermes.async.queue-size=1024
Appender Configuration¶
@Configuration
public class NativeLoggingConfig {
@Bean
public ConsoleAppender consoleAppender() {
ConsoleAppender appender = new ConsoleAppender();
appender.setLayout(new PatternLayout(
"%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"
));
appender.start();
return appender;
}
}
Performance Comparison¶
Startup Time¶
| Configuration | JVM | Native Image |
|---|---|---|
| Simple app | ~2.5s | ~0.05s |
| Spring Boot app | ~8s | ~0.15s |
Memory Usage¶
| Configuration | JVM | Native Image |
|---|---|---|
| Simple app | ~50MB | ~15MB |
| Spring Boot app | ~300MB | ~80MB |
Throughput¶
- Native image throughput is comparable to JVM after warmup
- No JIT warmup period with native image
- Consistent performance from the start
Troubleshooting¶
ServiceLoader Issues¶
Problem: Logger provider not found
Solution: Verify META-INF/services files are included:
Reflection Errors¶
Problem: Class not found or method not accessible
Solution: Add to reflect-config.json:
[
{
"name": "your.package.YourClass",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
}
]
Resource Loading¶
Problem: Configuration file not found
Solution: Add to resource-config.json:
{
"resources": {
"includes": [
{"pattern": "application\\.properties"},
{"pattern": "application-.*\\.properties"}
]
}
}
Build Failures¶
Problem: Native image build fails
Solution: Enable verbose logging:
native-image \
--no-fallback \
-H:+ReportExceptionStackTraces \
-H:+PrintClassInitialization \
-jar target/my-app.jar
Best Practices¶
1. Test in Native Mode Early¶
Build and test native images regularly during development:
2. Minimize Reflection¶
Hermes avoids reflection by design:
- Annotation processing at compile-time
- ServiceLoader for provider discovery
- No runtime proxy generation
3. Profile Build Time¶
Monitor native image build times:
Typical build times:
- Simple app: 1-2 minutes
- Spring Boot app: 3-5 minutes
4. Optimize Image Size¶
Reduce image size with build flags:
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+StaticExecutableWithDynamicLibC</buildArg>
<buildArg>--gc=G1</buildArg>
</buildArgs>
5. Use Async Logging¶
Async logging performs well in native images:
Complete Example¶
Application Code¶
import io.github.dotbrains.InjectLogger;
@InjectLogger
public class NativeApplication extends NativeApplicationHermesLogger {
public static void main(String[] args) {
NativeApplication app = new NativeApplication();
app.run();
}
public void run() {
log.info("Starting native application");
long startTime = System.currentTimeMillis();
performWork();
long duration = System.currentTimeMillis() - startTime;
log.info("Application completed in {}ms", duration);
}
private void performWork() {
log.debug("Performing work...");
// Application logic
}
}
pom.xml¶
<project>
<properties>
<hermes.version>1.0.0-SNAPSHOT</hermes.version>
<native.maven.plugin.version>0.9.28</native.maven.plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>io.github.dotbrains</groupId>
<artifactId>hermes-api</artifactId>
<version>${hermes.version}</version>
</dependency>
<dependency>
<groupId>io.github.dotbrains</groupId>
<artifactId>hermes-processor</artifactId>
<version>${hermes.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.github.dotbrains</groupId>
<artifactId>hermes-core</artifactId>
<version>${hermes.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.github.dotbrains</groupId>
<artifactId>hermes-processor</artifactId>
<version>${hermes.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.maven.plugin.version}</version>
<extensions>true</extensions>
<configuration>
<imageName>native-app</imageName>
<mainClass>com.example.NativeApplication</mainClass>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
Build and Run¶
# Build native image
mvn clean package -Pnative
# Run native executable
./target/native-app
# Output:
# 10:30:45.123 INFO com.example.NativeApplication - Starting native application
# 10:30:45.125 DEBUG com.example.NativeApplication - Performing work...
# 10:30:45.150 INFO com.example.NativeApplication - Application completed in 27ms
Docker Example¶
Dockerfile (Multi-stage build)¶
# Build stage
FROM ghcr.io/graalvm/native-image:21 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN ./mvnw clean package -Pnative -DskipTests
# Runtime stage
FROM ubuntu:22.04
WORKDIR /app
COPY --from=build /app/target/native-app .
EXPOSE 8080
ENTRYPOINT ["./native-app"]