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
- Use Constructor Injection: Prefer constructor injection over field injection
- Avoid Reflection: Minimize use of reflection and dynamic class loading
- Configure Resources: Explicitly configure all resources needed at runtime
- Test Native Image: Always test your application as a native image
- Use Native Profile: Create specific configuration for native image builds
- Monitor Memory: Native images have different memory characteristics
- 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.