In a GraalVM native image, why do classpath resources (like a .txt or .sql file loaded via ClassPathResource) sometimes fail to load at runtime, and what fixes it?
answer
- Closed-world = only build-time-reachable embedded
- getResourceAsStream returns null in native
- ResourceHints.registerPattern / resource-config.json
- @ImportRuntimeHints + RuntimeHintsRegistrar
- JVM run passes, native fails — test the image
basics
~20 sGraalVM's closed-world build only bundles resources it knows about. Files loaded reflectively at runtime aren't detected, so they're dropped. You must register them (via resource-config.json or Spring ResourceHints) so the build includes them in the image.
solid answer
~40 sA native image is built ahead-of-time under a closed-world assumption: only code and resources reachable at build time are embedded; everything else is excluded to keep the image small. Classpath resources loaded dynamically — e.g. `getClass().getResourceAsStream(name)` or Spring's `ClassPathResource`/`ResourceLoader` — often can't be traced statically, so they're not included and `getResource` returns null at runtime, typically surfacing as a FileNotFound-style failure. The fix is to declare them so the build embeds them: GraalVM's native `resource-config.json` (patterns of resource paths to include), or, in Spring, register a `ResourceHints` entry — usually via a `RuntimeHintsRegistrar` with `hints.resources().registerPattern("config/*.sql")`. Spring Boot's AOT engine registers many resources automatically, but app-specific ad-hoc resources still need explicit hints.
code
java · 17 linesimport org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
@Configuration
@ImportRuntimeHints(AppResourceHints.MyHints.class)
class AppResourceHints {
static class MyHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Ant-style pattern; embeds every matching file into the image
hints.resources().registerPattern("db/migration/*.sql");
hints.resources().registerPattern("templates/email.html");
}
}
}go deeper
Know the phrase 'closed-world' and that dynamically loaded resources must be registered or they go missing.
Can write a RuntimeHintsRegistrar with registerPattern and wire @ImportRuntimeHints.
Explains the JVM-passes/native-fails trap and uses the tracing agent to bootstrap config.
Weighs Spring hints vs raw JSON, knows glob-vs-regex pattern syntax difference and what Boot auto-registers.
## The core problem: closed-world assumption A **GraalVM native image** is a standalone executable produced by ahead-of-time (AOT) compiling your application. Unlike the JVM, which loads classes and resources lazily at runtime, the native-image builder analyzes the whole program **at build time** and embeds only what it can prove is reachable. This is the **closed-world assumption**: anything the static analysis doesn't see is left out of the final binary. This applies to reflection, proxies, JNI, serialization — and **resources**. ## What is a 'resource' here? A resource is any non-class file on the classpath: `application.yml`, `messages.properties`, `schema.sql`, templates, JSON, images, etc. On the JVM you read them with `Class.getResourceAsStream(...)`, `ClassLoader.getResource(...)`, or Spring's `ClassPathResource`/`ResourceLoader`. Because these are looked up **by name (a String) at runtime**, the builder generally cannot know which files you'll ask for, so it does **not** embed them by default. At runtime `getResourceAsStream` then returns `null`. ## Two ways to register resources **1. GraalVM native config — `resource-config.json`** (lives under `META-INF/native-image/<group>/<artifact>/`). You list glob-like patterns: ```json { "resources": { "includes": [ { "pattern": "config/.*\\.sql" } ] } } ``` The builder then copies matching files into the image. **2. Spring's programmatic hints — `ResourceHints`.** Spring's AOT layer generates the GraalVM JSON for you from Java-defined **RuntimeHints**. You implement `RuntimeHintsRegistrar`: ```java class MyHints implements RuntimeHintsRegistrar { public void registerHints(RuntimeHints hints, ClassLoader cl) { hints.resources().registerPattern("config/*.sql"); } } ``` and wire it with `@ImportRuntimeHints(MyHints.class)` on a `@Configuration` class. This is the idiomatic Spring approach and keeps the hint next to your code. ## What Spring Boot does automatically Spring Boot's AOT processing registers a large set of resources for you: `application.properties`/`application.yml` and profile variants, `banner.txt`, `META-INF/spring.factories`, `git.properties`/`build-info.properties`, logging configs, and resources referenced by known infrastructure. So you usually only need manual hints for **your own ad-hoc resources** loaded by a path the framework doesn't know about. ## Gotchas - **Directory listing doesn't work.** Native images don't support enumerating a classpath directory; you must register concrete files/patterns. - **Patterns differ.** `resource-config.json` uses regex; Spring's `registerPattern` uses Ant/glob-style (`*`, `**`, `?`). Don't mix syntaxes. - **Missing-resource symptoms are silent-ish.** You get a `null` stream or FileNotFound only at runtime in the native binary — the JVM run passes, hiding the bug. Always test the actual native image (or use `RuntimeHintsPredicates` in a unit test). - **The GraalVM tracing agent** (`-agentlib:native-image-agent`) run on the JVM can auto-capture accessed resources into config JSON — useful to bootstrap hints. ## When to use what Prefer Spring `RuntimeHintsRegistrar` + `@ImportRuntimeHints` inside a Spring app (portable, colocated, testable). Drop to hand-written `resource-config.json` for non-Spring libraries or when a third-party jar needs hints you ship yourself.
- Your app works on the JVM but the native binary throws because a .sql file is missing. What's the fastest way to discover which resources are actually accessed?Run the app on a normal JVM with the GraalVM tracing agent (`-agentlib:native-image-agent=config-output-dir=...`). It records every resource/reflection/proxy access and writes `resource-config.json` you can review and trim, then feed into the build.
- Does Spring register application.yml automatically, and why does that matter?Yes — Spring Boot's AOT processing auto-registers config files (application.properties/yml and profile variants), banner.txt, spring.factories, etc. That's why config loads in native without manual hints; you only add hints for your own non-standard resources.