skip to content

How do you run the native-image-agent and manage its output across multiple runs?

level: middleimportance: should knowfreq 45%

answer

  1. config-output-dir overwrites
  2. config-merge-dir accumulates
  3. META-INF/native-image/<group>/<artifact>
  4. caller-filter/access-filter trim noise
  5. exercise all paths (run tests)

basics

~10 s

Add -agentlib:native-image-agent=config-output-dir=<dir> to the JVM command and exercise the app. Use config-merge-dir instead of config-output-dir to accumulate metadata from several runs, and put the final JSON under META-INF/native-image/.

solid answer

~40 s

You attach the agent with `-agentlib:native-image-agent=config-output-dir=<dir>` on a plain `java` command and then drive the app through its features. `config-output-dir` overwrites the directory each run, so for multiple scenarios you switch to `config-merge-dir=<dir>`, which merges new observations into existing files without losing prior ones — a common pattern is one run with output-dir to seed, then merge subsequent runs. The generated `reflect-config.json`, `resource-config.json`, `proxy-config.json`, etc. go under `src/main/resources/META-INF/native-image/<group>/<artifact>/` so `native-image` auto-discovers them. You can also add `caller-filter-file`/`access-filter-file` to exclude noisy or irrelevant classes, and there are periodic-flush options. Because only executed paths are recorded, you typically run the agent over the test suite or a scripted exercise of all endpoints.

code

java · 18 lines
java
// 1) Seed metadata from a first scenario (overwrites the dir):
//   java -agentlib:native-image-agent=config-output-dir=out -jar app.jar

// 2) Add more scenarios WITHOUT losing the first (merges):
//   java -agentlib:native-image-agent=config-merge-dir=out -jar app.jar

// 3) Trim JDK/test-framework noise with a caller filter file:
//   -agentlib:native-image-agent=config-merge-dir=out,caller-filter-file=filter.json
//
// filter.json:
// {
//   "rules": [
//     { "excludeClasses": "org.junit.**" },
//     { "excludeClasses": "jdk.internal.**" }
//   ]
// }
//
// 4) Copy 'out' to src/main/resources/META-INF/native-image/<group>/<artifact>/

go deeper

for a junior

Know the -agentlib flag and that JSON lands under META-INF/native-image.

for a middle

Distinguish output-dir vs merge-dir and know the filter files and per-artifact folder convention.

for a senior

Design a run strategy (over tests) and use filters to keep metadata clean and reviewable.

for a principal

Automate collection in the build/CI, decide whom the metadata belongs to, and keep it namespaced and diffable.

**Invocation.** The agent is attached to an ordinary JVM launch: ``` java -agentlib:native-image-agent=config-output-dir=META-INF/native-image -jar app.jar ``` The part after `=` is a comma-separated option string. **Output vs merge — the crucial distinction:** - `config-output-dir=<dir>` — writes the metadata files, **overwriting** whatever was there. Good for the first run; bad if you have multiple scenarios, because each run clobbers the last. - `config-merge-dir=<dir>` — reads the existing files in `<dir>` and **merges** newly observed entries into them. This is how you accumulate metadata across many runs (different endpoints, different profiles). Typical flow: seed once with `config-output-dir`, then run every additional scenario with `config-merge-dir` pointing at the same directory. **Where the files go.** Native-image automatically scans `META-INF/native-image/**` on the classpath. The convention is a per-library subfolder: `src/main/resources/META-INF/native-image/<groupId>/<artifactId>/`, keeping metadata namespaced so it doesn't collide with other jars. **Generated files:** `reflect-config.json`, `resource-config.json`, `proxy-config.json`, `jni-config.json`, `serialization-config.json`, plus `predefined-classes-config.json` for bytecode defined at runtime. Recent GraalVM merges these into one `reachability-metadata.json`. **Filtering the noise.** Runs pick up reflection from the JDK, the app server, test frameworks, etc. Two filter options trim this: - `caller-filter-file=<json>` — ignore accesses *made by* matching classes (e.g. don't record what JUnit itself reflects on). - `access-filter-file=<json>` — exclude accesses *to* matching classes from the output. Each is a small JSON file with `includes`/`excludes` glob rules. This keeps the metadata focused on your code and target libraries. **Other useful options:** `config-write-period-secs` and `config-write-initial-delay-secs` for long-running apps that never cleanly exit (so metadata is flushed periodically); `experimental-conditional-config-*` for conditional metadata (see the conditional-config question). **Exercising the app.** Because the agent records only executed code, incomplete exercise = incomplete metadata = runtime failures in the native image. So you run it against something that touches every path — most commonly the **integration/functional test suite**, or a scripted client hitting every endpoint. Build tools (GraalVM Native Build Tools) formalize this with an `-Pagent` mode that runs `test` under the agent and a `metadataCopy` task to move the result into `META-INF/native-image`.

  • What is the difference between config-output-dir and config-merge-dir?
    config-output-dir overwrites the target directory on each run; config-merge-dir reads existing metadata and merges new observations in, so you can accumulate across multiple runs without losing earlier results.
  • Why might you use config-write-period-secs?
    For long-running or non-terminating apps (e.g. a server you stop with SIGKILL) that never reach a clean shutdown — periodic flushing writes metadata to disk incrementally instead of only on exit.

saying these in an interview costs you the question

  • Using config-output-dir for every scenario and wondering why only the last run's metadata survives
  • Putting metadata somewhere native-image doesn't scan (not under META-INF/native-image)
  • Assuming a single happy-path run captures everything

context