skip to content

What do SerializationHints and JNI hints do, and how do they differ from ordinary reflection hints?

level: seniorimportance: should knowfreq 30%

answer

  1. serialization() → serialization-config.json (Serializable/ObjectStream)
  2. jni() returns ReflectionHints → jni-config.json
  3. JNI = native C code calling into Java
  4. Reflection ≠ serialization ≠ JNI: separate files
  5. Rare in web apps; RMI/clustering/native libs

basics

~10 s

hints.serialization().registerType(T.class) enables Java serialization of a type in native (→ serialization-config.json). hints.jni() returns a ReflectionHints whose entries go to jni-config.json, for native code calling back into Java. Both are separate from ordinary reflect-config.json reflection.

solid answer

~50 s

`SerializationHints` (via `hints.serialization()`) declares types that use **Java's built-in serialization** (`Serializable`, `ObjectOutputStream`/`ObjectInputStream`). Serialization reflectively touches fields and needs `serialVersionUID`/constructor machinery, so GraalVM requires an explicit list — you call `registerType(Class)` or `registerType(TypeReference)`, written to `serialization-config.json`. It's separate from `ReflectionHints` because GraalVM models serialization as its own category with different metadata. **JNI hints** are reached via `hints.jni()`, which returns a `ReflectionHints` instance — the same API as ordinary reflection — but its registrations are emitted to `jni-config.json`, governing which Java members **native (C/C++) code** may access via the JNI bridge. So the difference is the audience and target file: `reflection()` → `reflect-config.json` for Java-side reflection; `jni()` → `jni-config.json` for native-side access; `serialization()` → `serialization-config.json` for Java serialization. All three are rarely needed in typical web apps but matter for RMI, distributed caches, JNI libraries, and legacy serialization.

code

java · 13 lines
java
import org.springframework.aot.hint.*;

public class SerializationAndJniHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader cl) {
        // Java serialization (e.g. clustered session / RMI payload)
        hints.serialization().registerType(SessionData.class);      // -> serialization-config.json

        // Members a native (C/C++) library reaches via JNI
        hints.jni().registerType(NativeCallback.class,             // -> jni-config.json
                MemberCategory.INVOKE_DECLARED_METHODS);
    }
}

go deeper

for a junior

Awareness only: serialization and JNI are extra hint kinds for special cases.

for a middle

Know serialization() enables Serializable types and jni() is for native-code access, each with its own file.

for a senior

Explain why they're separate categories, that jni() reuses ReflectionHints, and that reflection ≠ serialization.

for a principal

Advise when they're actually needed (RMI, clustering, native libs) and reason about field-graph coverage and the reachability-metadata consolidation.

## SerializationHints `org.springframework.aot.hint.SerializationHints` (from `RuntimeHints.serialization()`) targets **Java Object Serialization** — the `java.io.Serializable` mechanism where `ObjectOutputStream.writeObject`/`ObjectInputStream.readObject` serialize an object's field graph. This machinery works reflectively (it reads/writes private fields, invokes `readObject`/`writeObject`/`readResolve`, and uses the no-arg constructor of the first non-serializable superclass). GraalVM cannot infer which types will be serialized, so each must be declared: ```java hints.serialization().registerType(SessionData.class); hints.serialization().registerType(TypeReference.of("com.acme.CacheEntry")); ``` These entries go to `serialization-config.json`. It is a **separate category** from reflection because serialization needs extra, serialization-specific metadata beyond plain member access — hence a dedicated sub-API even though it too ultimately concerns reflective access. When you need it: distributed session stores, RMI, Hazelcast/Ignite/JMS payloads, any DTO passed through Java serialization. Modern apps favor JSON, so this is comparatively rare — but legacy and clustered systems still hit it. ## JNI hints `RuntimeHints.jni()` returns a `ReflectionHints` — literally the *same class* used by `reflection()`, exposing `registerType`, `registerMethod`, `registerField`, etc. The crucial difference: its registrations are written to `jni-config.json`, not `reflect-config.json`. **JNI (Java Native Interface)** is the bridge that lets native C/C++ code call into the JVM — and native code looks up Java classes/methods/fields by name at runtime, which is invisible to closed-world analysis just like Java reflection is. So if a native library (or GraalVM's own native code interop) needs to access your `com.acme.Callback.onEvent(...)` from C, you register it through `hints.jni()`: ```java hints.jni().registerType(NativeCallback.class, MemberCategory.INVOKE_DECLARED_METHODS); ``` Same API shape as reflection, different consumer (the native side) and different config file. ## Why they're distinct categories | Sub-API | Consumer | Config file | |---|---|---| | `reflection()` | Java code using `java.lang.reflect` | `reflect-config.json` | | `jni()` | Native (C/C++) code via JNI | `jni-config.json` | | `serialization()` | Java Object Serialization | `serialization-config.json` | GraalVM keeps them apart because although all three are 'reflective-ish', they are checked/enforced independently and carry different metadata. Registering a type for reflection does **not** make it serializable, and vice-versa; JNI access is gated separately from Java reflection even for the same member. ## Gotchas - A type reflectively accessible via `reflect-config.json` will still fail Java serialization unless *also* in `serialization-config.json`. - JNI hints are needed only when native code accesses Java — most pure-Spring apps never touch this; don't add them speculatively. - Serialization hints must include the whole field graph's types if those are custom and serialized. - As with all hints, omissions fail at runtime (e.g. `NotSerializableException`, JNI lookup failure), not at build time.

  • A type is registered in ReflectionHints but Java serialization of it still throws in native. Why?
    Reflection and serialization are independent GraalVM categories with separate config files. Reflection access (reflect-config.json) doesn't imply serializability. You must also register the type via hints.serialization().registerType(...) so it lands in serialization-config.json.
  • What does hints.jni() return, and why is that notable?
    It returns a ReflectionHints instance — the same type as hints.reflection(). The API is identical, but its entries are written to jni-config.json instead of reflect-config.json, governing access by native code through the JNI bridge rather than Java-side reflection.

saying these in an interview costs you the question

  • Assuming a reflection hint also enables Java serialization
  • Adding JNI hints for a pure-Java app with no native code
  • Thinking serialization/JNI/reflection all share one config file

context