Skip to content

v3.1 (4/N): GraalVM native-image hints + typed payload record#9

Merged
jlc488 merged 1 commit into
mainfrom
v3.1.0/graalvm-native-hints
May 23, 2026
Merged

v3.1 (4/N): GraalVM native-image hints + typed payload record#9
jlc488 merged 1 commit into
mainfrom
v3.1.0/graalvm-native-hints

Conversation

@jlc488
Copy link
Copy Markdown
Collaborator

@jlc488 jlc488 commented May 23, 2026

Summary

Makes the LLM-adapter modules native-image friendly. Two changes:

1. Replace Map.of(...) error payload with SsrfBlockPayload record

The previous form was an immutable Map.of(...) — internally one of ImmutableCollections.MapN, whose private layout Spring Boot's AOT processor couldn't introspect. On a ./gradlew nativeBuild, reflection on those classes would either fail or silently strip the field names, leaving the LLM with an empty {} body where it expected {"error":"ssrf_blocked","reason":"..."}.

The new record:

  • AOT sees the components from bytecode and registers reflection hints automatically
  • Wire shape gains stable declaration order (Map iteration order was officially undefined)
  • Same JSON keys, same values — existing substring-based tests pass verbatim

2. SsrfGuardLlmRuntimeHints + META-INF/spring/aot.factories

Registers what the AOT processor wouldn't trace on its own:

  • SsrfBlockPayload reflection (belt-and-braces — AOT usually catches records, but explicit removes guessing)
  • BlockReason enum reflection — Jackson reads enum constants + invokes label() via reflection

Loaded through META-INF/spring/aot.factories so Spring Boot's AotServices picks it up automatically whenever ssrf-guard-llm is on the classpath.

What doesn't need hints

  • All -springai / -langchain4j autoconfigs → Spring Boot 3 AOT handles @AutoConfiguration / @Bean / @Conditional* / @ConfigurationProperties out of the box
  • SsrfGuardReactorAddressResolverGroup → direct subclass of Netty's AbstractAddressResolver, no reflection
  • -httpclient5 / -jdkhttp / -okhttp → pure Java, no runtime reflection

Test plan

  • 9 existing JsonToolInputGuard tests pass after record refactor (proves wire-compat)
  • 2 new SsrfGuardLlmRuntimeHintsTest tests assert the right hints register via RuntimeHintsPredicates
  • Full build: 212 tests across 11 modules (up from 210 in PR v3.1 (3/N): docs + autoconfig integration tests #8)
  • CI green

Not in CI

Actual native-image smoke build (./gradlew nativeBuild on a demo) requires the GraalVM toolchain — adds ~10 min per run. The declarations here follow Spring Boot's documented patterns; if a consumer hits a native build issue, a one-line registerType(...) in the hints class is usually the fix. Tracking a CI-level native verification as a v3.2 candidate.

What's left in v3.1

  • Phase 6 — CHANGELOG entry final form, tag v3.1.0, Maven Central publish via release workflow, demo bumps in devslab-examples (including the new ssrf-guard-langchain4j-demo I'll write right after the tag goes out).

…Registrar)

Two changes that together make the library play nicely with Spring Boot
3's AOT processor and `./gradlew nativeBuild`:

1) Replace Map.of(...) error payload with the typed SsrfBlockPayload record

   The previous form serialised an immutable Map.of(...) — internally one
   of ImmutableCollections.MapN, whose private layout AOT couldn't
   introspect. Reflection on those classes would either fail at native-
   image build time or strip the fields silently, producing empty
   {} bodies in production where the LLM expected its structured error.

   The record fixes both: AOT sees the components at build time and
   registers reflection automatically, AND the JSON wire shape gains a
   stable declaration order (Map iteration order was officially
   undefined). The wire keys, values, and substring assertions remain
   unchanged — every existing JsonToolInputGuard test passes verbatim.

2) Add SsrfGuardLlmRuntimeHints + META-INF/spring/aot.factories

   - SsrfBlockPayload record reflection (INVOKE_PUBLIC_METHODS,
     constructors, DECLARED_FIELDS — covers everything Jackson touches)
   - BlockReason enum reflection (PUBLIC_FIELDS + methods — Jackson
     reads constants and calls label() via reflection)

   Spring Boot's AotServices loader discovers the registrar via
   `META-INF/spring/aot.factories`, so it activates whenever
   `ssrf-guard-llm` is on the classpath — no consumer code change.

What does NOT need its own hints

  - All -springai / -langchain4j autoconfigs — Spring Boot 3 AOT
    handles @autoConfiguration / @bean / @conditional* / @ConfigurationProperties
    out of the box.
  - SsrfGuardReactorAddressResolverGroup — extends Netty's
    AbstractAddressResolver, which is a regular subclass relationship;
    no reflection.
  - All -httpclient5 / -jdkhttp / -okhttp types — direct Java code,
    no runtime reflection.

Tests

  - Existing 9 JsonToolInputGuard tests pass after the record refactor
    (proves wire-compat).
  - 2 new SsrfGuardLlmRuntimeHintsTest tests assert the right hints
    are registered via Spring's RuntimeHintsPredicates.
  - Full build: 212 tests across 11 modules.

  Note: actual GraalVM native-image build verification isn't in CI
  yet — adding it requires the GraalVM toolchain on the runner and
  ~10 min build per smoke. The declarations here follow Spring Boot's
  documented patterns; if a consumer hits a native build issue, an
  additional registerType(...) call here is the fix.
@jlc488 jlc488 merged commit c69f78d into main May 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant