Skip to content

[WP8] Native-image / AOT: user-POJO hints uncovered + nativeTest never run in CI #185

Closed as not planned

Description

@shihyuho

此內容由 AI 產生(specification-mapper 全庫審查 2026-07-16)。

Work package WP8 · findings: COR-04, TEST-02 · worst severity: medium

From the 2026-07-16 whole-codebase review (branch jakarta, modules mapper/ + starter/). Each finding below is CONFIRMED by independent adversarial verification. File paths and line numbers reflect the review snapshot and may drift — treat them as starting points, not exact coordinates.


COR-04 — Native-image reflection hints cover only jars shipping their own Jandex index; user query POJOs are silently unfiltered, and the contract is undocumented

  • Category / severity: correctness / medium · effort M · confidence medium
  • Location: mapper/src/main/java/tw/com/softleader/data/jpa/spec/aot/SpecMapperRuntimeHints.java:50
  • Evidence: SpecMapperRuntimeHints registers hints only for classes found in classpath*:/META-INF/spec-mapper/reflect-config.idx; the only such index is produced for the mapper's own classes by jandex-maven-plugin (mapper/pom.xml:59-76). The starter ships no aot.factories or hints of its own (starter/src has none), and no documentation anywhere in the repo (site/content and README greps for jandex/reflect-config/native-image are empty) tells users to produce an index for their query POJOs or custom SimpleSpecification subclasses.
  • Failure scenario: A GraalVM native-image app uses the starter with a CustomerQuery POJO carrying @SPEC fields; the POJO gets no reflection metadata, so ReflectionDatabind's getDeclaredFields()/makeAccessible either sees zero fields (spec becomes null and findBySpec returns every row — filters silently dropped) or throws MissingReflectionRegistrationError, depending on GraalVM reachability-metadata mode; custom @SPEC(value=MySpec.class) classes likewise fail in SimpleSpecification.newSpec.
  • Suggested fix: Document the META-INF/spec-mapper/reflect-config.idx contract (jandex-maven-plugin snippet) for native-image users, and/or add an AOT BeanFactoryInitializationAotProcessor in the starter that registers hints for types reachable from repository QueryBySpecExecutor signatures.
  • Verification: [CONFIRMED] Traced the full mechanism and every load-bearing claim holds.
  1. SpecMapperRuntimeHints.java:49-58 registers reflection hints ONLY from resources matching classpath*:/META-INF/spec-mapper/reflect-config.idx, reading each as a Jandex index and registering every known class with all MemberCategory values. It never scans user application classes.

  2. mapper/pom.xml:59-73 shows the only producer of that index is the mapper module's own jandex-maven-plugin (indexDir target/classes/META-INF/spec-mapper, indexName reflect-config.idx). No other module/jar produces this path, so classpath*: matches only the mapper's own classes. Confirmed the starter ships no hints: only spring/aot.f…

TEST-02 — nativeTest profile is never exercised by any CI pipeline — GraalVM/AOT support can regress silently

  • Category / severity: testing / medium · effort M · confidence high
  • Location: Jenkinsfile:107
  • Evidence: The Makefile defines test-native (mvn -PnativeTest process-classes test) and the parent pom carries a full nativeTest profile (process-test-aot + native-maven-plugin test execution), but the Jenkinsfile's stages run only make compile and make test (Unit Tests stage, and Matrix Tests also make test), and the six GitHub workflows (bump-snapshot, bump-spring, commitlint, label-issues, release-please, site-build) contain no maven test invocation at all. The JVM-mode hint tests (SpecRuntimeHintsTest, DomainRuntimeHintsTest, AnnotationRuntimeHintsTest) check hint registration but never run code in an actual native image.
  • Failure scenario: A new SimpleSpecification subclass or annotation is added without landing in the Jandex index consumed by SpecMapperRuntimeHints (or a GraalVM/Spring AOT upgrade breaks processing); every JVM test stays green, the release ships, and native-image users hit reflection/instantiation failures at runtime.
  • Suggested fix: Add a CI stage (Jenkins or a GitHub Actions job with graalvm/setup-graalvm) running make test-native at least on the release Java/Spring combination.
  • Verification: [CONFIRMED] Traced every claim. Jenkinsfile lines 108 and 136 invoke only make test (which is mvn process-classes test) and the Compile stage runs make compile; no stage runs make test-native or -PnativeTest. Makefile confirms test-native: mvn -PnativeTest process-classes test, unused by CI. pom.xml:318-362 defines a full nativeTest profile (spring-boot process-test-aot + graalvm native-maven-plugin native-test goal). Grep across .github/workflows/ found no maven test invocation. The three RuntimeHints tests exist and are ordinary JVM tests that assert hint registration, never executing in a native image. So the nativeTest/AOT path is genuinely never exercised by any pipeli…

Full report: see the codebase-review-2026-07-16 branch (linked on the parent tracking issue).

Activity

  1. added theissue type on Jul 16, 2026
  2. shihyuho commented on Jul 16, 2026

    @shihyuho
    MemberAuthor

    This was generated by AI during triage.

    Triage Notes - needs human

    Category: bug (primary sub-finding COR-04 is a native-image correctness defect; TEST-02 is a CI/test-coverage gap)

    Summary: In a GraalVM native image the library registers reflection hints only for its own classes, so user query POJOs and custom specification subclasses get no metadata and either silently drop all filters or fail at runtime — and no CI stage exercises the native/AOT path, so this can regress undetected.

    Current behavior:

    • SpecMapperRuntimeHints (registered as a RuntimeHintsRegistrar via the mapper module's META-INF/spring/aot.factories) registers reflection hints exclusively from Jandex indexes found at classpath*:/META-INF/spec-mapper/reflect-config.idx. The only producer of that resource is the mapper module's own jandex-maven-plugin execution, so classpath*: matches only the library's own classes.
    • User application classes are never scanned. The starter module ships no aot.factories and no RuntimeHintsRegistrar of its own (only its AutoConfiguration.imports).
    • At query time ReflectionDatabind walks the user query POJO's fields via doWithLocalFields(target.getClass(), ...) and reads them with ReflectionUtils.makeAccessible + getField; SimpleSpecification.newSpec instantiates custom SimpleSpecification subclasses reflectively via accessibleConstructor(...).newInstance(...). In a native image without hints for those user types, getDeclaredFields returns nothing (spec built from zero fields → filters silently dropped → every row returned) or a MissingReflectionRegistrationError is thrown, and custom @Spec(value=...) classes fail to instantiate.
    • There is no documentation anywhere in the repo (README, site/content, CONTRIBUTING) describing the META-INF/spec-mapper/reflect-config.idx contract or telling native-image users how to make their query POJOs / custom specs reachable.
    • No CI pipeline exercises the native path: the Jenkinsfile runs only make compile, make test, and matrix make test; the six GitHub workflows invoke no Maven test at all. The test-native Makefile target (mvn -PnativeTest process-classes test) and the parent pom's nativeTest profile (process-test-aot + native-maven-plugin) exist but are never invoked by any pipeline. The three JVM hint tests (SpecRuntimeHintsTest, DomainRuntimeHintsTest, AnnotationRuntimeHintsTest) only assert hint registration on the JVM — they never build or run an actual native image.

    Desired behavior:

    • Native-image applications using the starter get correct behavior for their own @Spec-annotated query POJOs and custom SimpleSpecification subclasses — either automatically, or via a clearly documented, supported mechanism the user applies.
    • The native/AOT support is protected by CI so a new subclass/annotation, a Jandex-index miss, or a GraalVM/Spring AOT upgrade cannot ship a silent native-image regression.

    Key interfaces (named types/methods):

    • SpecMapperRuntimeHints (implements org.springframework.aot.hint.RuntimeHintsRegistrar) — the registerHints(RuntimeHints, ClassLoader) method and the REFLECT_CONFIG_LOCATION constant.
    • ReflectionDatabind — the of(...) factory that calls doWithLocalFields(target.getClass(), ...) and getFieldValue(...) using ReflectionUtils.makeAccessible.
    • SimpleSpecification#newSpec(Context, Class<? extends SimpleSpecification>, String, Object) — reflective accessibleConstructor(...).newInstance(...).
    • QueryBySpecExecutor (starter) — candidate anchor for an AOT BeanFactoryInitializationAotProcessor if the auto-registration route is chosen.
    • Build/CI: parent pom nativeTest profile, Makefile test-native target, Jenkinsfile stages, .github/workflows/.

    Acceptance criteria (depend on the chosen approach — see Needs human):

    • Native-image users have a working, supported path so their query POJOs and custom spec subclasses receive reflection metadata (auto-registered by the starter, and/or documented for the user to apply).
    • If a documented-contract approach is chosen: README/site documents the META-INF/spec-mapper/reflect-config.idx mechanism (including a jandex-maven-plugin snippet) for native-image users.
    • If an auto-processor approach is chosen: the starter registers reflection hints (all MemberCategory values) for the relevant user types, verified by an AOT/hints unit test in the starter module.
    • A CI stage runs the native/AOT test path (make test-native / -PnativeTest) on at least the release Java/Spring combination, wired into whichever pipeline the maintainers designate.

    Out of scope:

    • Changing how JVM-mode reflection/databind works.
    • Broadening classpath*:/META-INF/spec-mapper/reflect-config.idx semantics for the library's own classes.

    Needs human because:

    1. COR-04 requires a design / public-contract decision. The fix has genuinely different shapes with different public-API and support implications: (a) document the reflect-config.idx contract and require users to run jandex-maven-plugin on their own modules; (b) ship an automatic AOT BeanFactoryInitializationAotProcessor in the starter; or (c) both. Option (b) is not mechanically obvious: the user query POJO type is passed as a runtime argument to SpecMapper/QueryBySpecExecutor, not declared in any repository generic signature, so "types reachable from repository QueryBySpecExecutor signatures" does not actually capture the POJOs that need hints — deciding what to scan (component-scan for @Spec fields? entity graph? user opt-in annotation?), the default on/off, and the resulting public contract is a maintainer policy call, not a self-contained coding task.
    2. TEST-02 requires CI/infrastructure sign-off and an environment the JUnit suite cannot provide. Running make test-native needs a GraalVM native-image toolchain (e.g. graalvm/setup-graalvm) and a new Jenkins/GitHub Actions stage; this is release/CI infrastructure that cannot be self-verified with plain mvn test in an isolated worktree, and which pipeline owns it (and its cost on the build matrix) is a maintainer decision. The concrete CI acceptance criteria also cannot be finalized until the COR-04 approach is chosen, since that determines what the native test must assert.
  3. shihyuho commented on Jul 20, 2026

    @shihyuho
    MemberAuthor

    Superseded by #202 in the execution map #204, which carries the same findings (COR-04, TEST-02). This 2026-07-16 static plan is being retired in favour of that map.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions