google-site-verification: google61fe8ba583a51912.html
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