Are you struggling getting the configuration right for reflection in native images? Here are some tips and tricks to get you up and running!
Building native images can take a lot of time so trial and error is not the optimal way to get the right configuration. In this post we will zoom in to the terminology and explore some tools we can use to make our quest easier. For a more general introduction to native images please take a look at this previous post on micronaut and native images.
A complete overview of all features and configuration of native images go to the graalvm website here. The complete code for this post can be found on github here.
Test project setup
We will be using the micronaut cli to get started quick and because micronaut has very good GraalVM integration. For the project setup do the following.
mn create-app nl.sybrenbolandit.kungfu.kung-fu-api --build=gradle --lang=java
Now create a data class like this.
@Introspected
public class Kick {
private String name;
public Kick(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Now make a controller (and a service with some mock data) so this class is published. This is not the focus of this post so we will leave it as an exercise. I implemented an endpoint returning a random kick which you can find in the complete code here. The result looks something like this.

Now let’s create a dockerfile that creates the native image
./gradlew nativeImage
Now look in the build/classes/java/main/META-INF/native-image folder and see what this has given us. Based on the @Introspected annotation micronaut generated a reflect-config.json. It contains the reflection configuration at build time needed for the native image.
[ {
"name" : "nl.sybrenbolandit.kungfu.kick.Kick",
"allDeclaredFields" : true,
"allPublicMethods" : true,
"allDeclaredConstructors" : true
}, {
"name" : "nl.sybrenbolandit.kungfu.kick.Kick[]"
} ]
Manual configuration
For the sake of research let’s remove the @Introspected annotation for now. If we now build the native image we see the following when we try to call the endpoint.

The serialization to JSON makes use of reflection and there is no reflection configuration (of the Kick class). We can manually add the reflection configuration by copying the generated config and place it in the src/main/resources in the following folder structure convention.
META-INF/
└── native-image
└── groupID (here: nl.sybrenbolandit.kungfu)
└── artifactID (here: kung-fu-api)
└── reflect-config.json
Note that micronaut did generate a resource-config.json based on all the resources it found in src/main/resources.
Configuration agent
GraalVM ships with an native image agent which analyses all the classes that need reflection while running. So make sure you are running your application with a GraalVM jdk. And extend the gradle run task with the following arguments for the JVM.
run {
jvmArgs "-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/nl.sybrenbolandit.kungfu/kung-fu-api/"
}
We set the native-image-agent and specify the directory where the generated config can be written to. When we now run our application and call the api the agent will analyse which classes are used. Note that the configuration files will be generated when we stop the application.
Note that we need to call the application with multiple inputs to really cover all the classes needed to run. So integrate this into your workflow and update these configurations with every functionality you add.
Note also that we use config-merge-dir and not the standard config-output-dir. The first time this will give errors that these files are not yet defined but in further development this will be the prefered setting.
Now let’s take a look at the generated reflection config.
...
{
"name":"nl.sybrenbolandit.kungfu.kick.Kick",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allPublicMethods":true,
"allDeclaredConstructors":true
},
{
"name":"nl.sybrenbolandit.kungfu.kick.Kick[]"
},
{
"name":"sun.misc.Unsafe",
"fields":[{"name":"theUnsafe"}],
"methods":[
{"name":"copyMemory","parameterTypes":["java.lang.Object","long","java.lang.Object","long","long"] },
{"name":"getAndAddLong","parameterTypes":["java.lang.Object","long","long"] },
{"name":"getAndSetObject","parameterTypes":["java.lang.Object","long","java.lang.Object"] },
{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }
]
},
{
"name":"sun.nio.ch.SelectorImpl",
"fields":[
{"name":"publicSelectedKeys", "allowUnsafeAccess":true},
{"name":"selectedKeys", "allowUnsafeAccess":true}
]
}
]
We see the Kick class we defenitly need and a lot more classes!
Filters
Not all the classes in the generated configuration are needed for the application to run correctly. We can define some rules for the agent to exclude some of these classes.
{ "rules": [
{"excludeClasses": "sun.**"},
{"excludeClasses": "com.sun.**"},
{"excludeClasses": "java.**"},
{"excludeClasses": "javax.**"},
{"excludeClasses": "io.micronaut.**"}
]}
Note that these rules are executed in sequence so later rules can overrule or completely negate the previous ones.
Now we have to add a reference to these filter rules to the jvm arguments.
run {
jvmArgs "-agentlib:native-image-agent=" +
"config-merge-dir=src/main/resources/META-INF/native-image/nl.sybrenbolandit.kungfu/kung-fu-api/," +
"access-filter-file=reflection-filter/access-filter.json"
}
Now we have more controle over the generated config file.
Note that the agent generated more configuration files. Some of them are empty for we do not use these functionalities (for now). Others, like the resource-config.json is already generated by micronaut and can be deleted.
Hopefully you now have more confidence to setup reflection in native images and some basic tools to tackle related problems. Happy reflecting!