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.

Authors: Sergio del Amo

Micronaut Version: 4.6.3

1. 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.

2. What you will need

To complete this guide, you will need the following:

3. 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.

4. Create the Application

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

5. Reflection Example

5.1. Classes

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

reflectconfigjson/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/src/main/java/example/micronaut/StringCapitalizer.java
package example.micronaut;

public class StringCapitalizer {

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

5.2. Singleton

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

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

import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@Singleton (1)
public class StringTransformer {

    private static final Logger LOG = LoggerFactory.getLogger(StringTransformer.class);

    String transform(String input, String className, String methodName) {
        try {
            Class<?> clazz = Class.forName(className);
            Method method = clazz.getDeclaredMethod(methodName, String.class);
            return method.invoke(null, input).toString();
        } catch (ClassNotFoundException e) {
            LOG.error("Class not found: {}", className);
            return input;
        } catch (NoSuchMethodException e) {
            LOG.error("Method not found: {}", methodName);
            return input;
        } catch (InvocationTargetException e) {
            LOG.error("InvocationTargetException: {}", e.getMessage());
            return input;
        } catch (IllegalAccessException e) {
            LOG.error("IllegalAccessException: {}", e.getMessage());
            return input;
        }
    }
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.

5.3. Controller

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

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

import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.annotation.QueryValue;

@Controller("/transformer") (1)
public class StringTransformerController {

    private final StringTransformer transformer;

    public StringTransformerController(StringTransformer transformer) {  (2)
        this.transformer = transformer;
    }

    @Produces(MediaType.TEXT_PLAIN) (3)
    @Get("/capitalize{?q}") (4)
    String capitalize(@Nullable @QueryValue String q) { (5)
        String className = "example.micronaut.StringCapitalizer";
        String methodName = "capitalize";
        return transformer.transform(q, className, methodName);
    }

    @Produces(MediaType.TEXT_PLAIN) (3)
    @Get("/reverse{?q}") (4)
    String reverse(@Nullable @QueryValue String q) { (5)
        String className = "example.micronaut.StringReverser";
        String methodName = "reverse";
        return transformer.transform(q, className, methodName);
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /transformer.
2 Use constructor injection to inject a bean of type StringTransformer.
3 Set the response content-type to text/plain with the @Produces annotation.
4 Use the @QueryValue annotation to define the q to be provided in the query part of the URL. Micronaut will parse them from request and provide as arguments to the method.

6. 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 application:

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

As expected, the output is:

HELLO

7. 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.

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

7.1. GraalVM Installation

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

Java 21
sdk install java 21.0.5-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 21
sdk install java 21.0.2-graalce

7.2. 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
graalvmNative {
    binaries {
        main {
            imageName.set('mn-graalvm-application') (1)
            buildArgs.add('-Ob') (2)
        }
    }
}
1 The native executable name will now be mn-graalvm-application
2 It is possible to pass extra build arguments to native-image. For example, -Ob enables the quick build mode.

8. 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 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 wil provide the necessary reflection metadata.

8.1. Tests

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

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

import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.net.URI;

import static org.junit.jupiter.api.Assertions.*;

@MicronautTest (1)
class StringTransformerControllerTest {

    @Test
    void capitalize(@Client("/") HttpClient httpClient) { (2)
        BlockingHttpClient client = httpClient.toBlocking();
        URI uri = uri("capitalize");
        assertEquals("HELLO", client.retrieve(HttpRequest.GET(uri)));
    }

    @Test
    void reverse(@Client("/") HttpClient httpClient) { (2)
        BlockingHttpClient client = httpClient.toBlocking();
        URI uri = uri("reverse");
        assertEquals("olleh", client.retrieve(HttpRequest.GET(uri)));
    }

    private URI uri(String path) {
        return UriBuilder.of("/transformer") (3)
                .path(path)
                .queryParam("q", "hello")
                .build();
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.
3 UriBuilder API allows you to create a java.net.URI easily.

9. Testing the Application

To run the tests:

./gradlew test

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

10. 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.

10.1. Test Failures

Test will fail 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.

11. Handling Reflection

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

You will see learn how to do this several ways with Micronaut Framework

11.1. 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.

11.2. 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/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.

11.3. @ReflectionConfig

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

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

import io.micronaut.core.annotation.ReflectionConfig;

@ReflectionConfig(
        type = StringReverser.class,
        methods = {
                @ReflectionConfig.ReflectiveMethodConfig(name = "reverse", parameterTypes = {String.class})
        }
)
@ReflectionConfig(
        type = StringCapitalizer.class,
        methods = {
                @ReflectionConfig.ReflectiveMethodConfig(name = "capitalize", parameterTypes = {String.class})
        }
)
class GraalConfig {
}
1 @ReflectionConfig is a repeatable annotation that directly models the GraalVM reflection config JSON format.

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.

11.4. @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/src/main/java/example/micronaut/StringReverser.java
package example.micronaut;

import io.micronaut.core.annotation.ReflectiveAccess;

public class StringReverser {

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

import io.micronaut.core.annotation.ReflectiveAccess;

public class StringCapitalizer {

    @ReflectiveAccess (1)
    static String capitalize(String input) {
        return input.toUpperCase();
    }
}
1 @ReflectiveAccess annotation that can be declared on a specific type, constructor, method or field to enable reflective access just for the annotated element.

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

12. Next Steps

Learn more about:

12.2. GraalVM Cloud deployment Guides

13. Help with the Micronaut Framework

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

14. License

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