Skip to content

GraalVM Native Image Reflect Config Demystified

Updated: at 12:00 PM

Updated March 2026: As of GraalVM for JDK 23 (released September 2024), the individual metadata files (reflect-config.json, resource-config.json, jni-config.json, proxy-config.json, and serialization-config.json) have been consolidated into a single reachability-metadata.json. The concepts in this post still apply — the structure just lives in one file now. See the update section at the bottom for the current format.

One of the hardest things about compiling a Spring Boot application into a GraalVM native image is generating the necessary reflection, resource and proxy configurations needed for the native image runtime.

These files, placed in the src/main/resources/META-INF/native-image will inform the runtime about all of the java magic that would be happening in a JVM environment, that isn’t available in the native-image.

Note (2026): The individual files described in this post are deprecated as of GraalVM JDK 23. They’re still accepted by the toolchain, but the modern format consolidates them into reachability-metadata.json. See the update section at the bottom for details.

When we do a nativeCompile, some native-image metadata is generated by the build process. But it can only see what information is available at build-time. The recommended way of generating the additional data that isn’t available at build-time is by using the tracing-agent. It’s fairly documented here on the official GraalVM site

There is a major downside to this tracing-agent process; lets say we have a native-image application with a dozen api endpoints using spring-web-mvc. When a developer updates the application and adds a new endpoint, the metadata hasn’t necessarily been updated to make the native-image runtime aware of the new reflection metadata. So while the application may compile and run correctly, the endpoint will fail at runtime. An example of this will be shown in this post.

To complete the example, we would simply execute the application using the JVM and the tracing agent, and exercise the endpoint. This will inform the tracing agent of what needs to be added to the reflect-config.json and other files.

The goal of this post is to explain exactly what the reflect-config.json file is doing and what information the tracing agent looks to write.

Test Project

To accomplish this, we’re going to create a new project and go through a typical workflow to build and update a native-image application.

Image of Spring Initializr

We can use Spring Initializr to generate our test project.

Executing a native run will show the application successfully executing.

  $ ./gradlew nativeRun
  ...
  > Task :nativeRun
  
  .   ____          _            __ _ _
  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
  ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
  =========|_|==============|___/=/_/_/_/
  :: Spring Boot ::                (v3.2.4)
  
  ...  INFO 12345 --- [demo] [           main] com.example.demo.DemoApplication         : Starting AOT-processed DemoApplication using Java 21.0.2 with PID 12345
  ...  INFO 12345 --- [demo] [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
  ...  INFO 12345 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.023 seconds (process running for 0.053)

We need to add some reflection to upset the native-image application. Rather than linking to a github repository, this example is short enough that we can just include it here:

In the following example, we’re gonna call the class automatically in a PostConstruct completely through reflection. At build time, the application has no idea what we’re doing.

  @SpringBootApplication
  public class DemoApplication {
  public static void main(String[] args) {
     SpringApplication.run(DemoApplication.class, args);
  }
  @PostConstruct
  void test() throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
     Class<?> printerClass = Class.forName("com.example.demo.TestPrinter");
     // Class<?>.newInstance() is deprecated, so this example eventually won't work
     TestPrinter printer = (TestPrinter) printerClass.newInstance();
     Method printMethod = printerClass.getDeclaredMethod("print");
     printMethod.setAccessible(true);
     printMethod.invoke(printer);
     }
  }
  class TestPrinter {
     void print() {
        System.out.println("Hello World");
     }
  }

Now that we have some reflection, we’ll execute the same command and see what happens!

  $ ./gradlew nativeRun
  > Task :nativeRun FAILED
  
  .   ____          _            __ _ _
  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
  ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
  =========|_|==============|___/=/_/_/_/
  :: Spring Boot ::                (v3.2.4)
  
  ...  INFO 12346 --- [demo] [           main] com.example.demo.DemoApplication         : Starting AOT-processed DemoApplication using Java 21.0.2 with PID 12346
  ...  INFO 12346 --- [demo] [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
  ...  WARN 12346 --- [demo] [           main] o.s.c.support.GenericApplicationContext  : Exception encountered during context initialization - cancelling refresh attempt: 
     org.springframework.beans.factory.BeanCreationException: 
        Error creating bean with name 'demoApplication': com.example.demo.TestPrinter
  ... ERROR 12346 --- [demo] [           main] o.s.boot.SpringApplication               : Application run failed

Reviewing what we know about native-image compilation, this makes perfect sense.

Our specific error that was output in the stacktrace was

  Caused by: java.lang.NoSuchMethodException: com.example.demo.TestPrinter.<init>()

Lets first execute this application with the tracing agent and see if we can resolve our runtime issue by making the build aware of this reflection at build-time.

Tracing Agent Execution

First, we’re going to run a build with ./gradlew build to create our jvm executable.

Next, we’ll execute our tracing agent execution from the jvm application. We don’t need to explicitly run through any workflows so the tracing agent can see the JVM instantiations, since we do this in a post-construct.

java -agentlib:native-image-agent=config-merge-dir=./config -jar build/libs/demo-0.0.1-SNAPSHOT.jar

The output is what you’d expect from the jvm application version, namely “Hello World” was printed to the console via reflection.

However, in our ./config folder, we now have a handful of native-image metadata files.

Now the tracing agent records everything it can, so even though we’re looking for a single reflection configuration, we ended up with a 49Kb text file with a ton of internal spring reflection registrations. Many of these would be handled at build-time or through reachability metadata. Here’s an example of the type of things the tracing agent picks up additionally:

...
{
    "name":"org.springframework.context.annotation.ComponentScan",
    "queryAllDeclaredMethods":true
},
{
    "name":"org.springframework.context.annotation.ComponentScan$Filter",
    "queryAllDeclaredMethods":true
},
{
    "name":"org.springframework.context.annotation.Conditional",
    "queryAllDeclaredMethods":true
},
{
    "name":"org.springframework.context.annotation.Configuration",
    "queryAllDeclaredMethods":true
},
...

In most cases, just generating the metadata using the tracing agent is the simplest solution. However, for the sake of understanding what’s happening, we’re going to add the file by hand.

First, we’re going to create the location to inject these files into the build process; creating the src/main/resources/META-INF/native-image directory.

Inside, we’ll create the reflect-config.json file.

Directory Sample

Breaking Down Reflect-Config

Legacy format note: The reflect-config.json walkthrough below uses the per-file format, which is deprecated as of GraalVM JDK 23 but still accepted. The equivalent configuration in the new unified reachability-metadata.json format is shown at the end of this post.

The official GraalVM documentation has examples of all of the available input types for each of the metadata files generated by the tracing agent and used by the native-image build process.

When you need to figure out what to add, using that as a reference will describe the JSON.

Now, lets get into creating this file. Because it is fairly abstract, we’ll build the json as we go and update each piece, rather than display the final file with notes.

It starts with an empty array of json objects:

[{}]

Inside, we need to let the build know our class and it’s path

[{
    "name":"com.example.demo.TestPrinter", // Our classname and path
}]

We reflectively create an instance of the object, so we need to let the build know that this class has a constructor.

[{
    "name":"com.example.demo.TestPrinter", // Our classname and path
    "methods":[{
        "name":"<init>",      // representation of the constructor
        "parameterTypes":[]   // no constructor parameters
    }]
]}

Now finally, we need to make it aware of the print method.

[{
    "name":"com.example.demo.TestPrinter", // Our classname and path
    "methods":[{
        "name":"<init>",      // representation of the constructor
        "parameterTypes":[]   // no constructor parameters
    },{
        "name":"print",       // the name of our defined method
        "parameterTypes":[]   // no method parameters
    }]
]}

With just the contents above, we can now rerun our native-image and see if that was enough to inform the build process about the required reflection to execute as we expect!

...  INFO 96340 ... com.example.demo.DemoApplication         : Starting AOT-processed DemoApplication using Java 21.0.2 with PID 12347
...  INFO 96340 ... com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
Hello World
...  INFO 96340 ... com.example.demo.DemoApplication         : Started DemoApplication in 0.025 seconds (process running for 0.052)

Success!!

With the native-image builder made aware of the only required reflection, our app runs perfectly.

Comparing our java execution to our native-image execution shows the order of magnitude increase in startup time, one of the massive benefits of running as a native-image.

Native:  Started DemoApplication in 0.025 seconds (process running for 0.052)
JVM:     Started DemoApplication in 0.415 seconds (process running for 0.658)

Takeaways

Sometimes when writing spring boot native-image applications, we’ll have a complicated workflow that is hard or impossible to catch with the tracing-agent. In these instances, adding the reflection configuration by hand is a perfectly acceptable way to ensure the application will function correctly at runtime.

Update: reachability-metadata.json (GraalVM JDK 23+)

Since writing the original version of this post, GraalVM for JDK 23 (released September 2024) introduced a cleaner approach: all of the individual metadata files have been merged into a single unified file called reachability-metadata.json.

From the official GraalVM JDK 23 release notes:

“Streamlined Native Image reachability metadata configuration into a single file reachability-metadata.json. The formerly-used individual metadata files (reflection-config.json, resource-config.json, and so on) are now deprecated, but will still be accepted.”

The five files that are now deprecated:

Instead of separate files in META-INF/native-image, you now have a single reachability-metadata.json that uses top-level keys for each category:

{
  "reflection": [],
  "resources": { "includes": [], "excludes": [] },
  "jni": [],
  "serialization": []
}

The TestPrinter Example in the New Format

The reflect-config.json I built manually above looks like this in reachability-metadata.json:

{
  "reflection": [
    {
      "type": "com.example.demo.TestPrinter",
      "methods": [
        { "name": "<init>", "parameterTypes": [] },
        { "name": "print", "parameterTypes": [] }
      ]
    }
  ]
}

Two things to notice:

The rest of the structure (methods, parameterTypes, fields) stays the same.

Spring Boot AOT Integration

For Spring Boot 3+ applications, the Spring AOT engine has already been updated to emit reachability-metadata.json automatically when compiling for GraalVM native images. In most Spring Boot projects you won’t need to write this file by hand — the AOT processor handles it.

When you do need to write it manually (unusual dynamic reflection patterns, third-party libraries the reachability metadata repository doesn’t cover yet), the file goes here:

src/main/resources/META-INF/native-image/<group-id>/<artifact-id>/reachability-metadata.json

Further Reading


Previous Post
Stop Guessing Your GraalVM Native Image Metadata
Next Post
GraalVM Native Spring Boot vs Go — Build, Boot, and Benchmark