package example.micronaut;
public class StringReverser {
static String reverse(String input) {
return new StringBuilder(input).reverse().toString();
}
}
Table of Contents
- 1. Getting Started
- 2. What you will need
- 3. Solution
- 4. Create the Application
- 5. Reflection Example
- 6. Running the Application
- 7. Generate a Micronaut Application Native Executable with GraalVM
- 8. Invoke the Native Image
- 9. Testing the Application
- 10. Native Tests
- 11. Handling Reflection
- 12. Next Steps
- 13. Help with the Micronaut Framework
- 14. License
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:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOME
configured appropriately
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.
-
Download and unzip the source
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.
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.
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.
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
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:
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:
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.
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.
Native tests integrate with the agent. If you execute:
./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
:
[
{
"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.
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.
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
.
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();
}
}
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.1. Docker and GraalVM
12.2. GraalVM Cloud deployment Guides
-
Deploy a GraalVM Native Executable of an application or a function to AWS Lambda
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…). |