Why can't the native-image builder discover getDeclaredMethod / Class.forName targets automatically, and when can it partially succeed?
answer
- target is a data value, not a code ref
- analysis can't execute runtime logic at build
- constant literal => constant folding => auto-included
- computed/config name => opaque => declare it
- Spring AOT covers framework reflection
basics
~20 sThose calls take runtime values (a name string) that the build-time static analysis can't evaluate, so it can't know the target. It can partially succeed only when the argument is a constant it can fold at build time.
solid answer
~40 sThe native-image builder relies on static reachability analysis that traces the call graph and data flow at build time. `Class.forName(name)` and `getDeclaredMethod(name, types)` select their target from a runtime value; unless that value is a compile-time constant the analysis can fold, it is opaque, so the builder cannot mark the resulting class or method as reachable and cannot retain its metadata. Hence dynamic reflection must be declared. It can *partially* succeed when the arguments are constant literals — e.g. `Class.forName("com.acme.Fixed")` — because constant folding lets the analysis identify the exact target and include it automatically. Any non-constant, config-driven, or computed name defeats this and requires explicit registration (reflect-config.json / hints), which Spring AOT generates for framework patterns.
code
java · 7 lines// Partially succeeds: constant literal is folded, analysis resolves the target
Class<?> ok = Class.forName("com.acme.Fixed"); // auto-included
// Fails without declaration: names are computed at runtime
String name = base + "." + variant; // not a compile-time constant
Class<?> bad = Class.forName(name); // opaque to the analysis
Method m = bad.getDeclaredMethod(methodFromConfig); // opaque target methodgo deeper
Knows dynamic names can't be seen at build time.
Explains the data-value vs code-reference distinction and the constant-folding exception.
Knows getDeclaredMethod also needs parameter types resolvable and that folding is brittle to rely on.
Uses the folding boundary to decide where to design out reflection vs. declare candidate sets.
## The mechanism: static analysis vs. runtime values Native-image builds run a **points-to / reachability analysis** over your bytecode at build time. It models how values flow and which methods call which, to compute the reachable set. This analysis is powerful for *ordinary* code — a typed method call, a `new Foo()`, a field access — because the target is encoded directly in the bytecode. Reflection is different: the target is a **data value**, not a code reference. Consider: ```java Class<?> c = Class.forName(name); // target = value of 'name' Method m = c.getDeclaredMethod(mName); // target = value of 'mName' ``` To know which class/method to keep, the analysis would have to *evaluate* `name` and `mName` — but those may come from configuration, user input, a computation, or a database. The analysis cannot execute arbitrary runtime logic at build time, so these targets are **opaque**. Consequently the class isn't added to the reachable set (or its member metadata isn't retained), and the reflective call fails at runtime. ## When it can partially succeed: constant folding The one case the analysis *can* handle is when the argument is a **compile-time constant** it can trace through **constant folding**: ```java Class<?> c = Class.forName("com.acme.Fixed"); // literal -> analysis resolves it ``` Because the string never changes, the analysis identifies `com.acme.Fixed` exactly and can include it and register the reflective access automatically. This also extends to simple constant propagation — a `static final String` literal passed straight through. GraalVM implements these reflective-call intrinsics precisely so common constant patterns don't need manual config. But the moment the value becomes dynamic — `Class.forName(prefix + suffix)`, a config lookup, a switch on user input — folding fails and you're back to needing declarations. ## Practical consequences in Spring - **Framework reflection** (bean instantiation, `@ConfigurationProperties` binding, Jackson field access) is analysed and registered by **Spring AOT** during the build, so you don't manage it. *How* Spring emits those hints is a separate topic; the point here is that Spring compensates for the analysis's blindness by declaring the metadata itself. - **Your own dynamic reflection** — string-driven plugin loading, SPI dispatch by config — is *not* something Spring can infer, and constant folding won't save you because the names are dynamic. You must register the candidate set or, better, redesign to a statically-known dispatch. ## Gotchas - Don't assume `Class.forName` "sometimes works" arbitrarily — it works *only* when the analysis can prove the constant. Relying on incidental folding is brittle. - Concatenated or interned strings that *look* constant but are computed at runtime are **not** foldable. - `getDeclaredMethod` needs both the method name *and* parameter types resolvable; dynamic parameter types are just as opaque as dynamic names. ## When to care Only for native-image targets. On the JVM every reflective call resolves at runtime regardless. If shipping native, treat any non-constant reflective target as something you must declare or design away.
- Does a static final String make Class.forName foldable?If it is a genuine compile-time constant (a literal, not a runtime-computed value), the analysis can usually propagate and fold it, resolving the target. But a static field assigned from config or a computation at class-init time is not a compile-time constant and stays opaque.
- Why isn't Spring AOT enough to cover your own Class.forName(configValue)?Spring AOT infers hints from framework patterns it understands (beans, properties binding, JSON DTOs). It cannot know the value of your runtime config string or the finite set of classes it may name, so your dynamic reflection needs explicit registration or a redesign to statically-known dispatch.
saying these in an interview costs you the question
- Claiming the builder can trace any reflective call given enough analysis
- Assuming concatenated strings are constant-foldable
- Thinking Spring AOT covers arbitrary user-written Class.forName