Micronaut GraalVM Native Support in Java

Overview

Micronaut provides excellent support for GraalVM Native Image compilation, allowing you to build fast-starting, low-memory native executables. This is particularly useful for serverless functions, microservices, and CLI applications.

Setup and Configuration

1. Dependencies Setup

Maven Configuration:

<properties>
<micronaut.version>4.2.0</micronaut.version>
<graalvm.version>23.0.0</graalvm.version>
</properties>
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-jackson-databind</artifactId>
<scope>compile</scope>
</dependency>
<!-- GraalVM Native Image Dependencies -->
<dependency>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>svm</artifactId>
<version>${graalvm.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.micronaut.build</groupId>
<artifactId>micronaut-maven-plugin</artifactId>
<version>4.1.5</version>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.28</version>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<mainClass>com.example.Application</mainClass>
<imageName>my-native-app</imageName>
<buildArgs>
<buildArg>--verbose</buildArg>
<buildArg>--no-fallback</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>

Gradle Configuration:

plugins {
id("com.github.johnrengelman.shadow") version "8.1.1"
id("io.micronaut.application") version "4.1.5"
id("org.graalvm.buildtools.native") version "0.9.28"
}
micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("com.example.*")
}
}
graalvmNative {
binaries {
main {
imageName.set("my-native-app")
buildArgs.add("--verbose")
buildArgs.add("--no-fallback")
}
}
}
dependencies {
implementation("io.micronaut:micronaut-http-server-netty")
implementation("io.micronaut:micronaut-http-client")
implementation("io.micronaut:micronaut-jackson-databind")
// GraalVM
compileOnly("org.graalvm.nativeimage:svm:23.0.0")
}

2. Application Configuration

application.yml:

micronaut:
application:
name: native-demo
server:
port: 8080
netty:
default:
allocator: pooled  # Important for native image
jackson:
serialization:
write-dates-as-timestamps: false
# Native image specific configuration
graalvm:
check:
missing-config: true
reflect-config:
enabled: true

Native Image Configuration

1. Reflection Configuration

META-INF/native-image/com.example/my-app/reflect-config.json:

[
{
"name": "com.example.model.User",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"allPublicFields": true
},
{
"name": "com.example.model.Product",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "com.example.dto.ApiResponse",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
}
]

2. Resource Configuration

META-INF/native-image/com.example/my-app/resource-config.json:

{
"resources": {
"includes": [
{
"pattern": "application\\.yml$"
},
{
"pattern": "META-INF/services/.*"
},
{
"pattern": "logback\\.xml$"
},
{
"pattern": ".*\\.properties$"
}
]
},
"bundles": []
}

3. Serialization Configuration

META-INF/native-image/com.example/my-app/serialization-config.json:

[
{
"name": "com.example.model.User"
},
{
"name": "com.example.model.Product"
},
{
"name": "java.time.LocalDateTime"
},
{
"name": "java.time.LocalDate"
}
]

Practical Examples

Example 1: Native REST API

package com.example;
import io.micronaut.runtime.Micronaut;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Singleton;
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}

Controller:

package com.example.controller;
import io.micronaut.http.annotation.*;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Optional;
@Controller("/api/users")
public class UserController {
private final UserService userService;
@Inject
public UserController(UserService userService) {
this.userService = userService;
}
@Get(produces = MediaType.APPLICATION_JSON)
public List<User> getAllUsers() {
return userService.findAll();
}
@Get(uri = "/{id}", produces = MediaType.APPLICATION_JSON)
public Optional<User> getUserById(@PathVariable Long id) {
return userService.findById(id);
}
@Post(consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON)
@Status(HttpStatus.CREATED)
public User createUser(@Body User user) {
return userService.save(user);
}
@Put(uri = "/{id}", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON)
public User updateUser(@PathVariable Long id, @Body User user) {
user.setId(id);
return userService.save(user);
}
@Delete("/{id}")
@Status(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.deleteById(id);
}
}

Service:

package com.example.service;
import com.example.model.User;
import com.example.repository.UserRepository;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
@Singleton
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
public User save(User user) {
return userRepository.save(user);
}
public void deleteById(Long id) {
userRepository.deleteById(id);
}
}

Repository:

package com.example.repository;
import com.example.model.User;
import io.micronaut.core.annotation.NonNull;
import jakarta.inject.Singleton;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Singleton
public class UserRepository {
private final Map<Long, User> users = new ConcurrentHashMap<>();
private long nextId = 1;
@NonNull
public List<User> findAll() {
return new ArrayList<>(users.values());
}
@NonNull
public Optional<User> findById(@NonNull Long id) {
return Optional.ofNullable(users.get(id));
}
@NonNull
public User save(@NonNull User user) {
if (user.getId() == null) {
user.setId(nextId++);
}
users.put(user.getId(), user);
return user;
}
public void deleteById(@NonNull Long id) {
users.remove(id);
}
}

Model:

package com.example.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.ReflectiveAccess;
@Introspected
@ReflectiveAccess
public class User {
private Long id;
private String name;
private String email;
private int age;
// Default constructor for Jackson
public User() {}
@JsonCreator
public User(@JsonProperty("id") Long id, 
@JsonProperty("name") String name,
@JsonProperty("email") String email,
@JsonProperty("age") int age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}

Example 2: Native Database Application

Entity:

package com.example.entity;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.data.annotation.*;
import java.time.LocalDateTime;
@Introspected
@ReflectiveAccess
@MappedEntity("products")
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private String description;
private Double price;
private Integer stock;
@DateCreated
private LocalDateTime createdAt;
@DateUpdated
private LocalDateTime updatedAt;
// Constructors
public Product() {}
public Product(String name, String description, Double price, Integer stock) {
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

Repository:

package com.example.repository;
import com.example.entity.Product;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
Optional<Product> findByName(String name);
List<Product> findByPriceGreaterThan(Double price);
List<Product> findByStockGreaterThan(Integer stock);
List<Product> findByNameContains(String name);
}

Service:

package com.example.service;
import com.example.entity.Product;
import com.example.repository.ProductRepository;
import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;
@Singleton
@Transactional
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> findAll() {
return (List<Product>) productRepository.findAll();
}
public Optional<Product> findById(Long id) {
return productRepository.findById(id);
}
public Optional<Product> findByName(String name) {
return productRepository.findByName(name);
}
public Product save(Product product) {
return productRepository.save(product);
}
public void deleteById(Long id) {
productRepository.deleteById(id);
}
public List<Product> findExpensiveProducts(Double minPrice) {
return productRepository.findByPriceGreaterThan(minPrice);
}
public List<Product> findInStockProducts() {
return productRepository.findByStockGreaterThan(0);
}
public List<Product> searchProducts(String keyword) {
return productRepository.findByNameContains(keyword);
}
}

Configuration for Database:

micronaut:
application:
name: native-db-app
datasources:
default:
url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
driverClassName: org.h2.Driver
username: sa
password: ''
schema-generate: CREATE_DROP
dialect: H2
jpa:
default:
entity-scan:
packages:
- 'com.example.entity'
properties:
hibernate:
show_sql: true
format_sql: true
# Native image configuration for H2
native-image:
h2:
enabled: true

Example 3: Native CLI Application

package com.example.cli;
import io.micronaut.configuration.picocli.PicocliRunner;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.util.concurrent.Callable;
@Command(name = "native-cli", description = "A native CLI application built with Micronaut",
mixinStandardHelpOptions = true)
public class NativeCliCommand implements Callable<Integer> {
@Option(names = {"-v", "--verbose"}, description = "Verbose mode")
private boolean verbose;
@Parameters(index = "0", description = "The input file", arity = "1")
private File inputFile;
@Option(names = {"-o", "--output"}, description = "Output file")
private File outputFile;
@Option(names = {"-c", "--config"}, description = "Configuration file")
private File configFile;
public static void main(String[] args) {
// Use PicocliRunner for native image compatibility
int exitCode = PicocliRunner.execute(NativeCliCommand.class, args);
System.exit(exitCode);
}
@Override
public Integer call() throws Exception {
try (ApplicationContext context = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
FileProcessor processor = context.getBean(FileProcessor.class);
if (verbose) {
System.out.println("Processing file: " + inputFile.getAbsolutePath());
System.out.println("Output file: " + 
(outputFile != null ? outputFile.getAbsolutePath() : "stdout"));
}
if (!inputFile.exists()) {
System.err.println("Input file does not exist: " + inputFile.getAbsolutePath());
return 1;
}
try {
String result = processor.processFile(inputFile, configFile);
if (outputFile != null) {
processor.writeToFile(outputFile, result);
if (verbose) {
System.out.println("Result written to: " + outputFile.getAbsolutePath());
}
} else {
System.out.println(result);
}
return 0;
} catch (Exception e) {
System.err.println("Error processing file: " + e.getMessage());
if (verbose) {
e.printStackTrace();
}
return 2;
}
}
}
}

File Processor Service:

package com.example.cli;
import jakarta.inject.Singleton;
import java.io.*;
import java.nio.file.Files;
import java.util.stream.Collectors;
@Singleton
public class FileProcessor {
public String processFile(File inputFile, File configFile) throws IOException {
String content = Files.readString(inputFile.toPath());
// Simple processing - count words and lines
long lineCount = content.lines().count();
long wordCount = content.split("\\s+").length;
StringBuilder result = new StringBuilder();
result.append("File: ").append(inputFile.getName()).append("\n");
result.append("Size: ").append(inputFile.length()).append(" bytes\n");
result.append("Lines: ").append(lineCount).append("\n");
result.append("Words: ").append(wordCount).append("\n");
if (configFile != null && configFile.exists()) {
String config = Files.readString(configFile.toPath());
result.append("Config loaded: ").append(config.length()).append(" characters\n");
}
return result.toString();
}
public void writeToFile(File outputFile, String content) throws IOException {
Files.writeString(outputFile.toPath(), content);
}
}

Advanced Native Image Features

1. Custom Native Image Configuration

NativeImageConfiguration.java:

package com.example.config;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Singleton;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
@Factory
@Requires(env = "native")
public class NativeImageConfiguration {
@Singleton
public Feature customNativeFeature() {
return new Feature() {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Register classes for reflection at build time
registerForReflection("com.example.model.User");
registerForReflection("com.example.model.Product");
registerForReflection("com.example.dto.ApiResponse");
// Register resource patterns
registerResources("application\\.yml");
registerResources("logback\\.xml");
}
private void registerForReflection(String className) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
RuntimeReflection.register(clazz.getDeclaredConstructors());
RuntimeReflection.register(clazz.getDeclaredMethods());
RuntimeReflection.register(clazz.getDeclaredFields());
} catch (ClassNotFoundException e) {
System.err.println("Class not found for reflection: " + className);
}
}
private void registerResources(String pattern) {
// Resource registration would go here
// This is a simplified example
}
@Override
public String getDescription() {
return "Custom native image configuration for Micronaut application";
}
};
}
}

2. Runtime Initialization Configuration

META-INF/native-image/com.example/my-app/native-image.properties:

Args = --enable-url-protocols=http,https \
--initialize-at-build-time=io.micronaut \
--initialize-at-build-time=com.example \
--initialize-at-run-time=io.netty.channel.epoll \
--initialize-at-run-time=io.netty.channel.kqueue \
--report-unsupported-elements-at-runtime \
--allow-incomplete-classpath \
--no-fallback \
-H:+ReportExceptionStackTraces

3. Conditional Configuration for Native Image

package com.example.config;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Introspected;
@ConfigurationProperties("native")
@Requires(env = "native")
@Introspected
public class NativeConfiguration {
private boolean optimizeNetty = true;
private boolean usePooledAllocator = true;
private int maxInitialLineLength = 4096;
private boolean validateHeaders = false;
// Getters and setters
public boolean isOptimizeNetty() { return optimizeNetty; }
public void setOptimizeNetty(boolean optimizeNetty) { this.optimizeNetty = optimizeNetty; }
public boolean isUsePooledAllocator() { return usePooledAllocator; }
public void setUsePooledAllocator(boolean usePooledAllocator) { this.usePooledAllocator = usePooledAllocator; }
public int getMaxInitialLineLength() { return maxInitialLineLength; }
public void setMaxInitialLineLength(int maxInitialLineLength) { this.maxInitialLineLength = maxInitialLineLength; }
public boolean isValidateHeaders() { return validateHeaders; }
public void setValidateHeaders(boolean validateHeaders) { this.validateHeaders = validateHeaders; }
}

Testing Native Applications

1. Native Image Test Configuration

package com.example;
import io.micronaut.runtime.EmbeddedApplication;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import jakarta.inject.Inject;
@MicronautTest
class NativeApplicationTest {
@Inject
EmbeddedApplication<?> application;
@Test
void testItWorks() {
Assertions.assertTrue(application.isRunning());
}
}

2. Controller Tests

package com.example.controller;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@MicronautTest
class UserControllerTest {
@Inject
@Client("/")
HttpClient client;
@Test
void testGetAllUsers() {
List<User> users = client.toBlocking()
.retrieve(HttpRequest.GET("/api/users"), List.class);
assertNotNull(users);
assertTrue(users.isEmpty()); // Initially empty
}
@Test
void testCreateUser() {
User user = new User(null, "John Doe", "[email protected]", 30);
User created = client.toBlocking()
.retrieve(HttpRequest.POST("/api/users", user), User.class);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals("John Doe", created.getName());
assertEquals("[email protected]", created.getEmail());
}
}

Build and Deployment

1. Dockerfile for Native Image

FROM ghcr.io/graalvm/native-image:ol8-java17-22 AS builder
WORKDIR /home/app
COPY . .
RUN ./mvnw package -Dpackaging=native-image
FROM frolvlad/alpine-glibc:alpine-3.13
RUN apk update && apk add libstdc++
COPY --from=builder /home/app/target/my-native-app /app/my-native-app
EXPOSE 8080
ENTRYPOINT ["/app/my-native-app"]

2. Multi-stage Build with Maven

# Build stage
FROM maven:3.8.6-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# Native image stage
FROM ghcr.io/graalvm/native-image:ol8-java17-22 AS native
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
RUN native-image -jar app.jar --no-fallback -H:Name=myapp
# Runtime stage
FROM alpine:3.17
RUN apk add --no-cache libstdc++
COPY --from=native /app/myapp /app/myapp
EXPOSE 8080
ENTRYPOINT ["/app/myapp"]

3. Build Scripts

build-native.sh:

#!/bin/bash
# Build native image
./mvnw clean package -Dpackaging=native-image
# Check if build was successful
if [ $? -eq 0 ]; then
echo "Native image built successfully"
echo "File size: $(du -h target/my-native-app | cut -f1)"
echo "Starting application..."
./target/my-native-app
else
echo "Native image build failed"
exit 1
fi

Performance Optimization

1. Memory Configuration

application.yml:

micronaut:
server:
netty:
allocator: pooled
netty:
leak-detection-level: disabled
max-order: 3
# JVM-style memory settings for native image
native:
memory:
max-heap: 256M
stack-size: 1M

2. Logging Optimization

logback.xml for Native Image:

<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!-- Reduce logging for native image -->
<logger name="io.micronaut" level="WARN"/>
<logger name="io.netty" level="WARN"/>
</configuration>

Common Issues and Solutions

1. Reflection Configuration

For classes that need reflection, use @Introspected and @ReflectiveAccess:

@Introspected
@ReflectiveAccess
public class CustomDTO {
private String field;
// Must have no-args constructor for Jackson
public CustomDTO() {}
public CustomDTO(String field) {
this.field = field;
}
// Getters and setters are required
public String getField() { return field; }
public void setField(String field) { this.field = field; }
}

2. Resource Loading

Use ClassLoader resources carefully:

@Singleton
public class ResourceLoader {
public String loadResource(String path) {
// Use ClassLoader.getResourceAsStream for native compatibility
try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) {
if (is != null) {
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
} catch (IOException e) {
throw new RuntimeException("Failed to load resource: " + path, e);
}
return null;
}
}

3. Dynamic Class Loading

Avoid dynamic class loading in native images:

// Instead of this (won't work in native):
Class<?> clazz = Class.forName(className);
// Use dependency injection or static configuration:
@Inject
private MyService service;

Best Practices

  1. Use Constructor Injection: Prefer constructor injection over field injection
  2. Avoid Reflection: Minimize use of reflection and dynamic class loading
  3. Configure Resources: Explicitly configure all resources needed at runtime
  4. Test Native Image: Always test your application as a native image
  5. Use Native Profile: Create specific configuration for native image builds
  6. Monitor Memory: Native images have different memory characteristics
  7. Profile Performance: Use GraalVM tools to profile and optimize your native application

Micronaut's GraalVM native support enables building highly efficient, fast-starting applications ideal for cloud-native environments, serverless platforms, and resource-constrained deployments.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper