Java / Gradle

Generate reflection metadata for GraalVM Native Image

In this guide, you will see several methods to provide the metadata required for reflection to be used in a Micronaut application distributed as a GraalVM Native executable.

Sergio del Amo
On this guide
In this section

Getting Started

In this guide, we will create a Micronaut application written in Java.

In this tutorial, you are going to learn how to use Reflection in Native Image within a Micronaut application.

Java reflection support (the java.lang.reflect.* API) enables Java code to examine its own classes, methods, fields and their properties at run time.

Native Image supports reflection but needs to know ahead-of-time the reflectively accessed program elements.

Micronaut Framework internals do not use reflection. However, you may want to use reflection in your application or use a Java library that uses reflection.

This tutorial will create a Micronaut application that uses reflection, and then show different techniques to configure it to work with Native Image.

What you will need

To complete this guide, you will need the following:

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

Create the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

Reflection Example

Classes

Create two classes to transform a java.lang.String. You will invoke them via Java Reflection.

reflectconfigjson/java/src/main/java/example/micronaut/StringReverser.java
package example.micronaut;

public class StringReverser {

    static String reverse(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}
reflectconfigjson/java/src/main/java/example/micronaut/StringCapitalizer.java
package example.micronaut;

public class StringCapitalizer {

    static String capitalize(String input) {
        return input.toUpperCase();
    }
}

Singleton

Create a singleton class that transforms a java.lang.String via reflection.

reflectconfigjson/java/src/main/java/example/micronaut/StringTransformer.java

Controller

Create a controller, which exposes two routes, and calls the StringTransformer passing it a class name and method name.

reflectconfigjson/java/src/main/java/example/micronaut/StringTransformerController.java

Running the Application

To run the application, use the ./gradlew run command, which starts the application on port 8080.

You can execute the endpoint exposed by the application:

curl "localhost:8080/transformer/capitalize?q=Hello"

As expected, the output is:

HELLO

Generate a Micronaut Application Native Executable with GraalVM

We will use GraalVM, an advanced JDK with ahead-of-time Native Image compilation, to generate a native executable of this Micronaut application.

Compiling Micronaut applications ahead of time with GraalVM significantly improves startup time and reduces the memory footprint of JVM-based applications.

Note
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.

GraalVM Installation

The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.

Java 25
sdk install java 25.0.2-graal

For installation on Windows, or for a manual installation on Linux or Mac, see the GraalVM Getting Started documentation.

The previous command installs Oracle GraalVM, which is free to use in production and free to redistribute, at no cost, under the GraalVM Free Terms and Conditions.

Alternatively, you can use the GraalVM Community Edition:

Java 25
sdk install java 25.0.2-graalce

Native Executable Generation

To generate a native executable using Gradle, run:

./gradlew nativeCompile

The native executable is created in build/native/nativeCompile directory and can be run with build/native/nativeCompile/micronautguide.

It is possible to customize the name of the native executable or pass additional parameters to GraalVM:

build.gradle

Invoke the Native Image

You can execute the endpoint exposed by the native executable:

curl "localhost:8080/transformer/capitalize?q=Hello"
Hello

Transformation does not work in the native executable. The response is Hello instead of the expected HELLO.

You will see an ERROR log in the native image execution logs:

 ERROR example.micronaut.StringTransformer - Class not found: example.micronaut.StringCapitalizer

In the next sections, you will provide the necessary reflection metadata.

Tests

Write a test which should pass both for JIT or Native Image.

reflectconfigjson/java/src/test/java/example/micronaut/StringTransformerControllerTest.java

Testing the Application

To run the tests:

./gradlew test

Then open build/reports/tests/test/index.html in a browser to see the results.

Native Tests

The io.micronaut.application Micronaut Gradle Plugin automatically integrates with GraalVM by applying the Gradle plugin for GraalVM Native Image building.

This plugin supports running tests on the JUnit Platform as native images. This means that tests will be compiled and executed as native code.

To execute the tests, execute:

./gradlew nativeTest

Then open build/reports/tests/test/index.html in a browser to see the results.

INFO: A test may be disabled within a GraalVM native image via the @DisabledInNativeImage annotation.

Test Failures

The test will fail and you will see something like:

 JUnit Jupiter:StringTransformerControllerTest:reverse(HttpClient)
    MethodSource [className = 'example.micronaut.StringTransformerControllerTest', methodName = 'reverse', methodParameterTypes = 'io.micronaut.http.client.HttpClient']
    => org.opentest4j.AssertionFailedError: expected: <olleh> but was: <hello>
       org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
       org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
       org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
       org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
       org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
       org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1141)
       example.micronaut.StringTransformerControllerTest.reverse(StringTransformerControllerTest.java:29)
       java.base@17.0.9/java.lang.reflect.Method.invoke(Method.java:568)
       io.micronaut.test.extensions.junit5.MicronautJunit5Extension$2.proceed(MicronautJunit5Extension.java:142)
       io.micronaut.test.extensions.AbstractMicronautExtension.interceptEach(AbstractMicronautExtension.java:155)
       [...]

Test run finished after 74 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         3 tests found           ]
[         0 tests skipped         ]
[         3 tests started         ]
[         0 tests aborted         ]
[         1 tests successful      ]
[         2 tests failed          ]


FAILURE: Build failed with an exception.

Handling Reflection

You can use Reflection in Native Image. GraalVM analysis attempts automatic detection of reflection usage. If automatic analysis fails, you can manually provide the program elements reflectively accessed at run time.

You will learn several ways to do this with the Micronaut Framework.

Generating Reflection Metadata with GraalVM Tracing Agent

GraalVM provides a Tracing Agent to easily gather metadata and prepare configuration files.

./gradlew -Pagent nativeTest

It runs the tests on JVM with the native-image agent, collects the metadata and uses it for testing on native-image.

A reflect-config.json file is generated in the build/native/agent-output/test directory.

build/native
├── agent-output
│   └── test
│       ├── agent-extracted-predefined-classes
│       ├── jni-config.json
│       ├── predefined-classes-config.json
│       ├── proxy-config.json
│       ├── reflect-config.json
│       ├── resource-config.json
│       └── serialization-config.json

From that file, we will only include the entries related to the StringCapitalizer and StringReverser classes which appear in reflect-config-json.

reflect-config.json

Reflection metadata can be provided to the native-image builder by providing JSON files stored in the META-INF/native-image/<group.id>/<artifact.id> project directory.

Create a new file src/main/resources/META-INF/native-image/example.micronaut.micronautguide/reflect-config.json:

reflectconfigjson/java/src/main/resources/META-INF/native-image/example.micronaut.micronautguide/reflect-config.json
[
  {
    "name":"example.micronaut.StringCapitalizer",
    "methods":[{"name":"capitalize","parameterTypes":["java.lang.String"] }]
  },
  {
    "name":"example.micronaut.StringReverser",
    "methods":[{"name":"reverse","parameterTypes":["java.lang.String"] }]
  }
]

If you execute the Native Tests again, they will pass.

@ReflectionConfig

Delete the JSON file you created in the previous step. Replace it with a class with @ReflectConfig annotations.

reflectconfig/java/src/main/java/example/micronaut/GraalConfig.java

The Micronaut GraalVM Annotation processor visits the annotation and provides the reflection metadata with a GraalVM Feature.

build.gradle
annotationProcessor("io.micronaut:micronaut-graal")

The io.micronaut.application Micronaut Gradle Plugin automatically adds the micronaut-graal annotation processor, you don’t have to specify it.

If you execute the Native Tests again, they will pass.

@ReflectiveAccess

If you can access the code, as in this example, you can annotate the class or method being accessed with reflection with @ReflectiveAccess.

Delete the GraalConfig class and annotate StringReverser and StringCapitalizer methods with @ReflectiveAccess.

reflectiveaccess/java/src/main/java/example/micronaut/StringReverser.java
package example.micronaut;

import io.micronaut.core.annotation.ReflectiveAccess;

public class StringReverser {

    @ReflectiveAccess // 
    static String reverse(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}
reflectiveaccess/java/src/main/java/example/micronaut/StringCapitalizer.java

If you execute the Native Tests again, they will pass.

Next Steps

Help with the Micronaut Framework

The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.

License

Note
All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images).