skip to content

How do ResourceHints handle static resources versus resource bundles in a native image?

level: middleimportance: should knowfreq 45%

answer

  1. resources() → resource-config.json
  2. registerPattern for templates/SQL/txt
  3. registerResourceBundle(baseName) for i18n
  4. Bundle = locale-aware, base name not glob
  5. Non-class files excluded by default

basics

~10 s

hints.resources().registerPattern("...") bundles matching files (templates, properties) into the image. hints.resources().registerResourceBundle("messages") includes an i18n ResourceBundle base name. Both go into resource-config.json.

solid answer

~40 s

`ResourceHints`, via `hints.resources()`, controls two related things. First, arbitrary classpath **resources**: `registerPattern("db/migration/*.sql")` or `registerResource(new ClassPathResource(...))` tells the native image to embed those files so `getResourceAsStream` works at runtime — otherwise they're absent because they aren't code. Second, **resource bundles** for i18n: `registerResourceBundle("messages")` registers a `ResourceBundle` base name so `ResourceBundle.getBundle("messages", locale)` resolves the localized `.properties` variants. Resource bundles are a distinct concept because GraalVM handles them specially (it needs the base name plus locale-specific files), which is why Spring exposes a dedicated method even though both feed the same `resource-config.json`. You register bundles rather than just patterns so all locale suffixes are captured correctly.

code

java · 15 lines
java
import org.springframework.aot.hint.*;
import org.springframework.core.io.ClassPathResource;

public class ResourceHintsExample implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader cl) {
        // Plain resources embedded so getResourceAsStream works
        hints.resources().registerPattern("templates/*.mustache");
        hints.resources().registerResource(new ClassPathResource("banner.txt"));

        // i18n bundles: register the BASE NAME, not a file glob
        hints.resources().registerResourceBundle("messages");
        hints.resources().registerResourceBundle("ValidationMessages");
    }
}

go deeper

for a junior

Know templates/properties must be registered so getResourceAsStream works in native.

for a middle

Distinguish registerPattern (plain files) from registerResourceBundle (i18n base name) and know both feed resource-config.json.

for a senior

Explain locale fallback semantics and why bundles get a dedicated method; scope patterns to avoid bloat.

for a principal

Weigh binary-size vs coverage, and design a hint strategy for many locales / third-party message sources.

## The two responsibilities of `ResourceHints` `org.springframework.aot.hint.ResourceHints` (from `RuntimeHints.resources()`) covers everything that is a *file on the classpath* rather than compiled code. In a native image, non-class resources are **not** included by default, so any `getResourceAsStream`, template load, or SQL migration read would fail unless declared. It writes to GraalVM's `resource-config.json`. ### 1. Plain resources (patterns) ```java hints.resources().registerPattern("templates/*.mustache"); hints.resources().registerPattern("db/migration/V*.sql"); hints.resources().registerResource(new ClassPathResource("banner.txt")); ``` `registerPattern` takes a pattern (with `*` wildcards) matched against classpath resource paths; matching files get embedded in the binary. `registerResource(Resource)` embeds a single known resource. There's also `registerPattern(ResourcePatternHint...)` style building for include/exclude patterns. ### 2. Resource bundles (i18n) Java internationalization uses `ResourceBundle.getBundle("messages", locale)`, which at runtime looks up `messages.properties`, `messages_fr.properties`, `messages_de_DE.properties`, etc. GraalVM treats bundles as a **special category** — you register a *base name*, and the toolchain arranges for all locale variants to be reachable: ```java hints.resources().registerResourceBundle("messages"); hints.resources().registerResourceBundle("validation/ValidationMessages"); ``` This is the concept the leaf calls 'ResourceBundleHints' — it isn't a separate top-level sub-API but a dedicated method on `ResourceHints`, because bundle resolution is locale-aware and GraalVM models it distinctly (in the classic format, bundles appear in a `bundles` section of `resource-config.json`). ## Why the distinction matters If you registered `messages*.properties` as a raw *pattern*, the files would be embedded, but you'd bypass GraalVM's bundle-awareness (locale fallback chains, `ResourceBundle.Control`, etc.). Using `registerResourceBundle` records the base name so bundle lookup semantics are preserved. Conversely, a bundle registration is the wrong tool for a non-`.properties` resource like an HTML template — use a pattern there. ## When you need these - Bean Validation messages (`ValidationMessages.properties`) — often needs a bundle hint in native - Custom `messages.properties` for `MessageSource` - Template engines reading `.mustache`/`.ftl`/`.html` from the classpath - Flyway/Liquibase-style SQL migration files, `.json`/`.yaml` config read via `getResourceAsStream` ## Gotchas - Spring Boot auto-registers many common resources, but *your* custom paths and third-party bundles frequently need manual hints. - Patterns match resource *paths*, not filesystem globs — get the classpath-relative path right. - A missing resource hint surfaces at runtime as a `null` stream or `MissingResourceException`, not a build error. - Over-broad patterns (e.g. `**/*`) bloat the binary — scope them.

  • Why register a resource bundle by base name instead of registering messages*.properties as a pattern?
    Bundle resolution is locale-aware: getBundle walks a fallback chain (messages_fr_FR → messages_fr → messages). Registering the base name preserves that bundle semantics and the locale variants; a raw pattern just embeds files and can miss GraalVM's bundle handling. Base name is the correct abstraction for i18n.
  • A .sql migration file read via getResourceAsStream returns null in native but works on the JVM — cause?
    The file wasn't embedded in the image because non-class resources are excluded by default. Add hints.resources().registerPattern("db/migration/V*.sql") (or the exact classpath path) so it's bundled into the binary.

saying these in an interview costs you the question

  • Assuming classpath resources are automatically included in native images
  • Registering i18n .properties as a plain pattern and losing locale fallback
  • Thinking ResourceBundleHints is a separate top-level sub-API rather than a method on ResourceHints

context