REFLECTION IN NATIVE IMAGES

REFLECTION IN NATIVE IMAGES

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        

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

No alt text provided for this image

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.

No alt text provided for this image

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!

To view or add a comment, sign in

More articles by Sybren Boland

  • UPGRADE TO MICRONAUT 3

    Are you curious what new features Micronaut version 3 brings and do you want to know how to upgrade your applications?…

  • ERC20 TOKEN

    Do you want to create your own ERC20 token on Ethereum? Here we program one and learn more on solidity while we’re at…

  • TESTCONTAINERS

    Are you struggling finding the right strategy for your integration tests? Mocking does not cover everything and relying…

    1 Comment
  • PICOCLI

    Do you want to bring your java applications to your command line? Picocli is a framework that enables you to create…

Others also viewed

Explore content categories