GraalVM readiness checks
The GraalVM panel surveys the host application for GraalVM native-image readiness and can generate a reachability-metadata.json scaffold from the scan. This page lists every check that ships with BootUI today, what it inspects, when it fires, and what to do about it.
Each check is a small class registered in GraalVmCheckRegistry and implemented in GraalVmChecks.java. The list intentionally stays compact and reviewable; adding a new check means adding one focused class plus a registry entry. The rule engine (checks, categories, the dependency scanner, and the reachability-metadata scaffold generator) is framework-neutral and lives in bootui-engine; today it is surfaced only through thin Spring adapter wiring in bootui-spring-autoconfigure/src/main/java/io/github/jdubois/bootui/autoconfigure/graalvm/ (the controller, the Dockerfile generator, and the source-tree writer).
What BootUI does
The scanner detects the host application's base package(s) from the @SpringBootApplication configuration via AutoConfigurationPackages, imports the compiled .class files from those packages with ArchUnit's ClassFileImporter, and evaluates every registered check against the imported classes. Importing is bounded to the application's own base package(s) — never the entire classpath — and runs only on demand when the scan action is invoked, caching the last report in the controller.
In addition to the checks, the scan does two things:
- Surveys classpath dependencies (when the Include dependencies toggle is on; it is on by default) to report which third-party JARs already ship bundled reachability metadata under
META-INF/native-image/. BootUI counts metadata only when a.jsonfile exists under that directory; a JAR that only hasnative-image.propertiesis reported as bundling native-image build arguments, not reachability metadata. The survey opens only classpath JARs, stops after 500 JARs, and adds a warning when that cap is hit; libraries without bundled metadata may need your own configuration, repository metadata, or the tracing agent. A single classpath entry can expand into several reported dependencies: when the application runs as a Spring Boot fat/uber jar,java.class.pathonly ever contains the outer launcher jar (Spring Boot'sLaunchedURLClassLoaderresolvesBOOT-INF/lib/*.jarthrough customnested:URLs that never populate that system property), so the survey expands the launcher jar into one inspected dependency per nestedBOOT-INF/lib/library instead of misreporting it as a single dependency named after the application's own launcher jar. When a shaded/uber jar (built with, for example, the Maven Shade or Gradle Shadow plugin) bundles more than oneMETA-INF/maven/<groupId>/<artifactId>/pom.properties— one for itself and one for each dependency it relocated into itself — the survey prefers the descriptor whoseartifactId/versionmatches the jar's own file name, recovering the shaded jar's own coordinates in the common case rather than misreporting it under one of the dependencies it relocated. - Builds a GraalVM 25
reachability-metadata.jsonscaffold from the application's own classes — unified reflection, serialization and JNI registrations, standard configuration/logging resource globs, and explicit proxy/Unsafe/FFM completion guidance when those calls are detected — which you can download from the panel. - Installs the scaffold into the source tree when the application is detectably running from an exploded build (for example
mvn spring-boot:runor an IDE) rather than a packaged jar. The Write into project action writes the scaffold tosrc/main/resources/META-INF/native-image/<groupId>/<artifactId>/reachability-metadata.json(coordinates resolved frombuild-info.propertiesor the projectpom.xml, falling back to abootui-generatednamespace). The write is confined undersrc/main/resourcesand refuses to overwrite areachability-metadata.jsonthat BootUI did not generate. - Generates a tailored
Dockerfile-nativefor the host application — a multi-stage build that detects the project's build system (Maven or Gradle, with or without the wrapper) and compiles a GraalVM native image with the matching command (./mvnw/mvn -Pnative -DskipTests clean native:compile, or./gradlew/gradle nativeCompile), then packages the resulting executable (named after the resolvedartifactId) into a minimal, distroless runtime image (gcr.io/distroless/base-debian12:nonroot). That base runs as a non-root user and ships glibc but no shell, package manager, curl, perl or tar, so the runtime's OS-package CVE surface stays near zero; the native image is built mostly static (only glibc is linked dynamically) so it needs no extra libraries. Because distroless has no shell or curl there is no DockerHEALTHCHECK- probe/actuator/health(or the web root) from your orchestrator instead. When the project carries no wrapper, the build stage installs a known, pinned Maven/Gradle release (declared as a constant in the generator and exposed as a DockerARG) so the image is self-contained. You can download it, or — under the same exploded-build constraint as the scaffold install — write it to the project root. That write is fail-closed and refuses to overwrite aDockerfile-nativethat BootUI did not generate. - Writes both artifacts in one step. The scaffold and the
Dockerfile-nativeare offered in a three-drawer accordion whose default, top All files drawer generates and writes both files into the source tree in a single action — under the same exploded-build constraint and the same fail-closed guards — and reports each file's outcome individually.
When BootUI is installed through bootui-spring-boot-starter, ArchUnit is included transitively so the panel works without an extra application dependency. The panel is available only when ArchUnit is on the classpath and a base package is resolvable from the running application. If no classes can be imported, the panel degrades to a stable, empty report with an explanatory reason rather than failing.
Separately from the panel scan, BootUI registers Spring AOT runtime hints for its own native-image needs from BootUiRuntimeHints. Those built-in hints cover BootUI's runtime-scanned classpath resources, BootUI DTO records used by Jackson, and the well-known reflective calls used by the Heap Dump, Security, and Pentesting panels. They are contributed by BootUiAutoConfiguration, so applications using the starter should not need to copy BootUI-specific hints into their own native-image configuration.
What BootUI does not do
- It is not a replacement for the GraalVM tracing agent or an actual native-image build. Static analysis cannot see reflection driven by runtime data, so the checks are heuristic review prompts, and the generated metadata is a scaffold to review and complete — not a finished file.
- It does not analyze third-party dependency bytecode for readiness; for dependencies it only reports whether classpath JARs ship bundled reachability metadata JSON or native-image build arguments.
- It does not modify, compile, or instrument application code; it reads already-compiled bytecode.
- Spring-managed beans are already covered by Spring AOT, so findings that overlap with Spring's own AOT processing may be safe to ignore.
Detecting missing metadata at development time
BootUI's checks are static, build-independent heuristics; they cannot see reflection driven by runtime-only data (for example, a class name read from a config file), so a clean scan is not a guarantee that a native image will run correctly. GraalVM's own recommended complement to static analysis is to make missing metadata fail loudly during development instead of surfacing as a silent runtime bug:
- Pass
--exact-reachability-metadata(introduced in GraalVM 23 for debugging and still opt-in in GraalVM 25), or, to scope exact handling to specific packages,--exact-reachability-metadata=<comma-separated-package-list>, tonative-imageat build time to opt in to the stricter, more debuggable handling of reflection, resources, JNI and serialization. Use--exact-reachability-metadata-path=<classpath-or-module-path-entry>when exact handling should apply to all types originating from selected path entries. - Run the native image with
-XX:MissingRegistrationReportingMode=Warnto see every place a registration is missing without crashing, or with-XX:MissingRegistrationReportingMode=Exit— recommended for automated tests — to make the application print the error with a full stack trace and exit immediately the first time a missing registration is hit, including ones a broadcatch (Throwable t)would otherwise silently swallow.
See the "Reachability Metadata" reference for the authoritative, up-to-date flag documentation. BootUI does not implement this as an automated check: unlike every other check on this page, which fires only when a specific risky bytecode/reflection construct is present, these flags are a blanket recommendation for essentially every native-image build regardless of what the code does — there is no bytecode condition to scan for, so an automated check would either fire unconditionally (defeating the panel's "only show what needs review" design) or require parsing the project's build file (pom.xml / build.gradle) to detect existing native-image arguments, a data source no other check depends on. It is listed here as a recommended practice to pair with the panel's static checks, not as another unconditional check.
The generated reachability-metadata.json
The scaffold follows the GraalVM 25 unified reachability metadata schema. Serialization is represented by serializable: true on a reflection registration (not the legacy top-level serialization array), and classes declaring native methods receive jniAccessible: true. Each named type carries a condition.typeReached guard. Reflection candidates include concrete records and Serializable types plus JPA entities and mapped superclasses, including abstract persistence base types. Resource globs cover application*.properties / application*.yml / application*.yaml, logback-spring.xml, and log4j2-spring.xml.
Static bytecode analysis cannot reliably recover runtime-computed proxy interface arrays, the Class argument passed to Unsafe.allocateInstance, or FFM FunctionDescriptor layouts. When those checks fire, the generated file therefore adds explicit review instructions rather than inventing unsafe registrations. FFM findings also scaffold the schema-valid foreign object with empty downcalls, upcalls, and directUpcalls arrays for the developer or tracing agent to complete. Dynamic proxies use a structured reflection type such as {"type":{"proxy":["com.example.Interface"]}}; FFM entries require the real memory-layout descriptors.
Review the generated file with the tracing agent, then place it under src/main/resources/META-INF/native-image/<groupId>/<artifactId>/ in your application. The panel substitutes the resolved groupId/artifactId into that hint whenever it can determine them — from build-info.properties (which works even when running from a packaged jar) or the project pom.xml — and keeps the <groupId>/<artifactId> placeholders only when no coordinates can be resolved.
Severity scale
Severity reflects the worst plausible impact if the finding is real, not the likelihood:
- CRITICAL — a construct with the most severe native-image impact if the finding is real. No active GraalVM check currently emits this severity.
- HIGH — a construct native images generally cannot support or Spring AOT cannot safely capture at run time (runtime class generation or Java compilation, script-engine discovery without a native integration, runtime classpath scanning, runtime instance suppliers, bean-referencing expression conditions, secondary context creation, dynamic/model MBeans).
- MEDIUM — a construct GraalVM cannot resolve at build time that will usually fail at run time without metadata (reflection, dynamic class loading, deep reflection, unsafe allocation, dynamic proxies, active JDK serialization, SpEL, method handles, frozen AOT conditions, custom security providers, runtime singleton registration).
- LOW — a construct that often needs extra configuration (runtime resource loading, resource bundles, service loading, reflective annotation access, static-initializer side effects, native access, native methods, JMX, foreign functions).
- INFO — an informational prompt that only matters if the type is actually used that way (serialization).
The scan evaluates every registered check, but the panel only lists checks that found something to review. Findings are ordered by importance (CRITICAL, HIGH, MEDIUM, LOW, INFO), then by the number of occurrences, and include up to a handful of sample detail lines.
Reflection
GRAAL-REFLECT-001 — Reflective API usage may need reflection metadata
- Severity: MEDIUM
- Inspects: calls to the reflection API (
Class.forName,Class.newInstance,Classmethod/field/constructor lookups and declared variants,Method.invoke,Constructor.newInstance, andFieldvalue get/set accessors). Reflective metadata accessors such asField.getName()are intentionally ignored. - Fires when: an application class uses those reflection APIs; constant targets may be resolved by native-image, but runtime-computed reflective targets need explicit metadata.
- Recommendation: register the reflectively accessed types in
reachability-metadata.json, or for application code register them with Spring's RuntimeHints (e.g. via@ImportRuntimeHints/RuntimeHintsRegistrar). Spring AOT already covers Spring-managed beans.
GRAAL-REFLECT-002 — Dynamic class loading may need reflection metadata
- Severity: MEDIUM
- Inspects: calls to
ClassLoader.loadClass. - Fires when: an application class loads a class by name at run time, which native-image cannot resolve at build time.
- Recommendation: register the dynamically loaded types under
reflectioninreachability-metadata.json, or replaceClassLoader.loadClasswith direct class literals where possible.
GRAAL-REFLECT-003 — Deep reflection (setAccessible / private lookups) may need reflection metadata
- Severity: MEDIUM
- Inspects:
AccessibleObject.setAccessible/trySetAccessibleandMethodHandles.privateLookupIn. - Fires when: a class uses deep reflection that bypasses access checks, which native-image must be told about to keep the members reachable.
- Recommendation: register the accessed members (with
allowWritewhere needed) underreflectioninreachability-metadata.jsonand ensure the required module opens are configured; prefer public APIs over deep reflection.
GRAAL-REFLECT-004 — Reflective annotation access may need reflection metadata
- Severity: LOW
- Inspects: reflective annotation queries (
getAnnotation,getDeclaredAnnotations,isAnnotationPresent, …) on reflected members (Method,Field,Constructor,Parameter). Reads onjava.lang.Classand otherAnnotatedElementsubtypes (Package,Module,RecordComponent) are intentionally ignored — only calls whose receiver is exactly one of those four member types are flagged. - Fires when: a class reads annotations from a reflected member whose annotations native-image only retains when the element is registered for reflection.
- Recommendation: register the inspected members under
reflectioninreachability-metadata.jsonso their annotations are available at run time.
GRAAL-REFLECT-005 — Unsafe.allocateInstance bypasses construction and needs unsafeAllocated metadata
- Severity: MEDIUM
- Inspects: calls to
allocateInstance(Class)onsun.misc.Unsafeorjdk.internal.misc.Unsafe. - Fires when: a class allocates an instance via
Unsafeinstead of a constructor. Unsafe allocation bypasses the construction path native-image's reachability analysis tracks, so the allocated type needs its own metadata; otherwise the call throwsMissingReflectionRegistrationErrorat run time. - Recommendation: register the allocated type under
reflectioninreachability-metadata.jsonwith"unsafeAllocated": true(in addition to its normal type registration), or replaceUnsafe.allocateInstancewith a public constructor or factory method where possible.
Dynamic proxies
GRAAL-PROXY-001 — Dynamic JDK proxies may need proxy metadata
- Severity: MEDIUM
- Inspects: calls to
Proxy.newProxyInstanceandProxy.getProxyClass. - Fires when: a class creates or obtains a JDK dynamic proxy whose interface list must be known to native-image. When the interface array is a compile-time constant, native-image may auto-register the proxy (similar to how constant-arg
Class.forNameis auto-folded); runtime-computed interface sets always need explicit registration. - Recommendation: declare the proxied interfaces in
reachability-metadata.json, or for application code register them with Spring's RuntimeHints (RuntimeHints.proxies().registerJdkProxy(...)via@ImportRuntimeHints). Spring's own proxy mechanisms are covered by Spring AOT.
Resources
GRAAL-RES-001 — Runtime resource loading may need resource metadata
- Severity: LOW
- Inspects: calls to
Class/ClassLoadergetResourceandgetResourceAsStream. - Fires when: a class loads a resource by name that must be embedded in the native image to be available at runtime. Calls with a constant resource name are often already detected automatically by native-image; runtime-computed names always need registration.
- Recommendation: register the loaded resource paths (as globs) in
reachability-metadata.json, or for application code register them with Spring's RuntimeHints (RuntimeHints.resources()via@ImportRuntimeHints) so native-image bundles them.
GRAAL-RES-002 — Resource bundle loading may need resource-bundle metadata
- Severity: LOW
- Inspects: calls to
ResourceBundle.getBundle. - Fires when: a class loads a localized resource bundle whose
.propertiesfiles must be embedded in the native image. - Recommendation: register the bundle base names under
bundlesinreachability-metadata.jsonso native-image includes every locale variant.
Service loading
GRAAL-SERVICE-001 — Service loading may need service metadata
- Severity: LOW
- Inspects: calls to
ServiceLoader.load/loadInstalled. - Fires when: a class discovers providers through
META-INF/services, which native-image must reach and reflectively instantiate. - Recommendation: ensure the
META-INF/servicesprovider files are on the classpath and register the provider implementations underreflectioninreachability-metadata.json.
Serialization
GRAAL-SER-001 — Serializable types may need serialization metadata
- Severity: INFO
- Inspects: application classes that implement
java.io.Serializable(concrete, non-enum types). - Fires when: an application type implements
Serializable; types that are actually serialized at runtime require serialization metadata. If GRAAL-SER-002 (active JDK serialization) also fires, the listed types are likely serialized at runtime and should be reviewed carefully. Enum types are excluded because GraalVM handles standard enum serialization automatically. - Recommendation: if these types are serialized (e.g. via the JDK serialization protocol), register them under
serializationinreachability-metadata.json.
GRAAL-SER-002 — Active JDK serialization may need serialization metadata
- Severity: MEDIUM
- Inspects: calls to
ObjectOutputStream.writeObject/writeUnsharedandObjectInputStream.readObject/readUnshared. - Fires when: a class serializes or deserializes types through the JDK serialization protocol at run time, which native-image must be told about explicitly.
- Recommendation: register every serialized type under
reflectionwithserializable: truein GraalVM 25's unifiedreachability-metadata.jsonschema (or with Spring's RuntimeHints serialization registration), or prefer a serialization format that does not need build-time registration.
Build-time initialization
GRAAL-INIT-001 — Build-time-initialized classes must not perform static I/O or start threads
- Severity: LOW
- Inspects: static initializers that directly perform file I/O (
java.iofile streams or filesystem-touchingjava.nio.file.Filescalls) or start threads / processes (Thread.start,Runtime.exec,ProcessBuilder.start). LightweightFilesmetadata predicates such asexists,isDirectory, andisReadableare intentionally ignored; side effects in helper methods or lambdas are out of scope. - Fires when: a class runs I/O or starts a thread/process from its static initializer. This is a review prompt only: since GraalVM 21.3+ classes are run-time-initialized by default, it matters only when the class is explicitly initialized at build time via the native-image
--initialize-at-build-timeflag. Spring AOT is one way to arrive at that configuration (it can compute and pass the flag for Spring-managed classes on the application's behalf), but the flag itself — not Spring AOT — is the actual mechanism native-image reads, so this also applies to build-time initialization configured directly or by other means. - Recommendation: if the class is listed under
--initialize-at-build-time, move the side effect out of the static initializer or switch the class to--initialize-at-run-timeso the I/O or thread starts when the application runs rather than during the native build.
GRAAL-INIT-002 — Build-time-initialized classes must not capture build-machine state
- Severity: LOW
- Inspects: static initializers that read environment- or time-sensitive state (
System.getenv/getProperty/getProperties, current time,java.timenow(), defaultLocale/TimeZone,InetAddress,Random/SecureRandomconstructors ornext*calls,UUID.randomUUID). - Fires when: a class captures those values in a static initializer. This is a review prompt only: since GraalVM 21.3+ classes are run-time-initialized by default, it matters only when the class is explicitly initialized at build time via the native-image
--initialize-at-build-timeflag. Spring AOT is one way to arrive at that configuration (it can compute and pass the flag for Spring-managed classes on the application's behalf), but the flag itself — not Spring AOT — is the actual mechanism native-image reads, so this also applies to build-time initialization configured directly or by other means; GraalVM's simulated class-init safely refuses unsafe initializers in ambiguous cases. - Recommendation: if the class is listed under
--initialize-at-build-time, move the state capture into a runtime code path or switch the class to--initialize-at-run-timeso the values are read when the native image starts rather than baked in during the build.
Native access
GRAAL-NATIVE-001 — Native libraries or unsupported Unsafe operations need native-image review
- Severity: LOW
- Inspects: loading of native libraries (
System.loadLibrary,System.load,Runtime.loadLibrary,Runtime.load) and unsupportedUnsafe.defineClass/defineAnonymousClassoperations. - Fires when: a class loads a native library or attempts runtime class definition through
Unsafe. OrdinaryUnsafememory access is intentionally not flagged: GraalVM 25 supports common Unsafe field/memory patterns and rewrites static-final field offsets where possible.Unsafe.allocateInstancehas its own precise GRAAL-REFLECT-005 check. - Recommendation: confirm loaded native libraries are available to the native image and add JNI configuration when needed; replace Unsafe runtime class definition with build-time generation.
GRAAL-NATIVE-002 — Native method declarations may need JNI configuration
- Severity: LOW
- Inspects: application classes that declare
nativemethods. - Fires when: a class declares a
nativemethod whose JNI entry point and backing native library must be configured for the native image. - Recommendation: in GraalVM 25's unified schema, register the declaring type under
reflectionwithjniAccessible: trueand the required method descriptors, then ensure the native library is bundled with and loadable by the native image. BootUI seeds this declaring-type registration.
GRAAL-FFM-001 — Foreign Function downcalls/upcalls may need foreign metadata in native images
- Severity: LOW
- Inspects: application classes that depend on
java.lang.foreign.Linker. - Fires when: a class uses
Linkerto build native downcall handles or upcall stubs; those down/upcalls reach native symbols that the closed-world analysis cannot see and must be described underforeigninreachability-metadata.json. Pure heap/off-heapMemorySegmentorArenausage that never touchesLinkerdoes not require this metadata and is not flagged. - Recommendation: register the real native down/upcall descriptors under
foreigninreachability-metadata.json, and pass--enable-native-access=<module-name>(orALL-UNNAMEDfor classpath code) for modules that perform restricted native operations. FFM support is enabled by default starting with GraalVM 25, but metadata and native-access permission solve separate problems. BootUI emits emptyforeignarrays as a safe scaffold; it does not invent function layouts.
Class generation
GRAAL-CLASSGEN-001 — Runtime class generation has no general-purpose support in native images
- Severity: HIGH
- Inspects: runtime bytecode/class generation (
ClassLoader.defineClass,MethodHandles.Lookup.defineClass/defineHiddenClass/defineHiddenClassWithClassData, CGLIBEnhancer, ByteBuddy, Javassist). - Fires when: a class generates or defines classes at run time. A closed-world native image has no compiler at run time, so these calls are not supported for arbitrary, build-time-unknown bytecode. The native-image agent's experimental "Predefined Classes" mode (
experimental-class-define-support) can trace and replay a bounded set of previously-seen classes, but it is best-effort: it replays only the exact bytecode traced ahead of time, allows only one class definition per class loader per execution, has no build-time-initialization support, and cannot help when classes are generated with varying names or bytecode (e.g. driven by counters or timestamps) — so it is a narrow escape hatch, not a general fix. - Recommendation: generate the classes at build time (e.g. via Spring AOT / build-time processing) instead of at run time, or replace the dynamically generated types with statically compiled equivalents. If runtime class generation truly cannot be avoided, evaluate the native-image agent's experimental Predefined Classes support as a narrow fallback — but note its known limitations before relying on it.
GRAAL-JDK-001 — The system Java compiler is unavailable in native images
- Severity: HIGH
- Inspects: calls to
javax.tools.ToolProvider.getSystemJavaCompiler(). - Fires when: application code requests
javacat run time. A native executable contains application code compiled ahead of time and cannot load newly compiled Java classes into the closed world. - Recommendation: compile or generate code during the application build and include the resulting classes in the native image; do not compile Java source inside the running application.
GRAAL-JDK-002 — JSR-223 script engines require native-image-specific support
- Severity: HIGH
- Inspects: construction of
javax.script.ScriptEngineManager. - Fires when: application code discovers JSR-223 engines at run time. The manager uses service loading, and engines commonly load or generate executable code dynamically. This is not a blanket claim that every engine is impossible: GraalVM languages can expose documented JSR-223 integrations, but that explicit language/runtime setup must be part of the image.
- Recommendation: remove runtime scripting, replace it with statically compiled application logic, or validate a specific engine's Native Image integration and register all service, resource, reflection, and native requirements.
Classpath scanning
GRAAL-SCAN-001 — Runtime classpath scanning does not work in native images
- Severity: HIGH
- Inspects: runtime classpath/component scanning (
ClassPathScanningCandidateComponentProvider.findCandidateComponents, the Reflections library, or ClassGraph). - Fires when: a class scans the classpath at run time; the closed-world native image has no scannable classpath at run time.
- Recommendation: resolve the scanning at build time. For Spring components rely on Spring AOT/component indexing rather than runtime scanning; replace library-based scanning with an explicit, statically known set of types.
Spring AOT
@ImportResource is intentionally not a warning. Spring's AOT refresh invokes configuration parsing and BeanFactoryPostProcessor implementations at build time, so statically declared imported bean definitions are part of the generated context. Runtime-selected or mutable XML is subject to the same fixed-build-input constraint as other bean definitions, but the annotation alone is not a high-confidence readiness problem.
SPRING-AOT-001 — Runtime bean singleton registration is not captured by Spring AOT
- Severity: MEDIUM
- Inspects: calls to
SingletonBeanRegistry.registerSingleton(...). - Fires when: a class adds beans to the context at run time; Spring AOT processes the bean factory at build time, so dynamically registered singletons are invisible to the AOT-generated context and native-image. Note: the singleton instance itself is present at runtime; the risk is only for reflective bean construction and AOT-generated context completeness.
- Recommendation: register the bean through standard build-time configuration (
@Bean/@Component/BeanFactoryInitializationAotContribution) so Spring AOT can see it. For programmatic AOT-aware registration, consider Spring Framework 7'sBeanRegistrarAPI.
SPRING-AOT-002 — Programmatic instance suppliers are not captured by Spring AOT
- Severity: HIGH
- Inspects: bean definitions backed by a programmatic instance supplier (
setInstanceSupplier, or SpringregisterBean/BeanDefinitionBuildermethods with aSupplier). - Fires when: a class registers a bean definition whose instance comes from a supplier lambda; Spring AOT cannot trace through that supplier at build time, so the bean's type and dependencies may be missing from the native image.
- Recommendation: prefer declarative bean definitions (
@Beanmethods / component scanning) whose types Spring AOT can resolve, or use Spring Framework 7'sBeanRegistrar/BeanRegistrarDslfor AOT-friendly programmatic registration; alternatively provide aRuntimeHintsRegistrarthat registers the supplied type for reflection. - Exclusion: Spring AOT-generated
*__BeanDefinitionsclasses intentionally use instance suppliers while replaying generated bean definitions. They are output of AOT processing, not unsupported application registration, and are excluded.
SPRING-AOT-003 — Environment-sensitive bean conditions freeze selection at AOT build time
- Severity: MEDIUM
- Inspects:
@Profile,@ConditionalOnProperty, custom@Conditional, and@ConditionalOnExpressionon application@Configuration/@Component(and stereotype) classes or@Beanmethods. - Fires when: a Spring component or
@Beanmethod carries a profile or property condition. Spring AOT evaluates these conditions once at build time; if the active profiles or application properties differ between the AOT build and the production runtime, the conditioned beans may be unexpectedly absent or present in the native image. - Stronger expression treatment: a
@ConditionalOnExpressionthat contains an explicit SpEL bean reference (@beanName) is HIGH. Spring Boot documents that such a reference initializes that bean very early, before normal post-processing such as configuration-properties binding, in addition to the general AOT build-time freeze. Property-only expressions retain MEDIUM severity. - Exclusion: deliberate
@AutoConfigurationclasses are condition-driven by design and are handled by Spring's AOT processing, so they are not reported. Build-time classpath conditions such as@ConditionalOnClassare not reported merely for being normal auto-configuration patterns. - Recommendation: ensure the profiles and properties active during the AOT build (native-image compilation) match the intended production configuration, or restructure the configuration to use explicit build-time selection rather than runtime conditions. Replace bean-referencing expression conditions with property/class conditions where possible.
SPRING-AOT-004 — Runtime ApplicationContext creation outside main entry point is not AOT-processed
- Severity: HIGH
- Inspects: constructor calls to
AnnotationConfigApplicationContextorGenericApplicationContext, andSpringApplicationBuilder.child(...)calls. - Fires when: a class programmatically creates a secondary application context; such contexts are never processed by Spring AOT, so their beans, runtime hints, and configuration are absent from the native image.
- Recommendation: consolidate configuration into the main application context processed by Spring AOT, or use
@Import/@ImportResourceto include additional configuration statically at build time.
GRAAL-SPEL-001 — Programmatic SpEL expression parsing relies on reflection with no AOT visibility
- Severity: MEDIUM
- Category: this check moved from
ReflectiontoSpring AOT— SpEL reachability is a Spring-library-specific concern (the SpEL bytecode compiler and reflective property access are part of Spring's own AOT story), not a general-purpose reflection construct. The check ID is unchanged (GRAAL-SPEL-001, not renumbered into theSPRING-AOT-*sequence) because check IDs are stable identifiers persisted in user dismissals. - Inspects: calls to
ExpressionParser.parseExpressionandparseRaw(the SpEL programmatic parsing API). - Fires when: a class parses a SpEL expression at run time; the parsed expression uses reflection to access object properties that is not visible to native-image, and the SpEL bytecode compiler is unsupported in native images.
- Recommendation: replace programmatic SpEL with direct Java code or annotation-driven evaluation (
@PreAuthorize,@Value,@Cacheable) that Spring AOT processes statically. If programmatic SpEL is required, register all reflectively accessed types underreflectioninreachability-metadata.json.
Method handles
GRAAL-MH-001 — Non-constant MethodHandle lookups may need reflection metadata
- Severity: MEDIUM
- Inspects: calls to
MethodHandles.Lookuplookup methods:findVirtual,findStatic,findConstructor,findSpecial,findGetter/findSettervariants,unreflectandunreflect*variants, andfindVarHandle/findStaticVarHandle. - Fires when: a class performs a
MethodHandles.Lookuplookup; non-constant method handles require reflection metadata for the target members that is not visible to the existing REFLECT checks. For compile-time-constant handles, native-image may fold the lookup automatically. - Recommendation: register the target members under
reflectioninreachability-metadata.jsonso native-image retains the necessary member descriptors.
Security providers
GRAAL-SEC-001 — Custom security providers may not initialize correctly in native images
- Severity: MEDIUM
- Inspects: calls to
Security.addProvider/Security.insertProviderAtand application classes that extendjava.security.Provider. - Fires when: a class registers a custom or third-party security provider (e.g. BouncyCastle) or extends
Providerdirectly; such providers rely on reflection-based service registration that is invisible to native-image. - Recommendation: register the provider and all its service implementations under
reflectioninreachability-metadata.json. Many providers (BouncyCastle, Conscrypt) publish a native-image integration guide or companion module.
JMX
GRAAL-JMX-001 — JMX usage requires --enable-monitoring in the native image
- Severity: LOW
- Inspects: calls to
ManagementFactory.getPlatformMBeanServerandMBeanServer.registerMBean. - Fires when: a class uses JMX; JMX is disabled by default in native images and requires
--enable-monitoring=jmxserverplus MBean reflection metadata. - Recommendation: add
--enable-monitoring=jmxserverto the native-image build arguments and register all MBean interfaces and implementations underreflectioninreachability-metadata.json.
GRAAL-JMX-002 — Dynamic/model MBeans are not supported by native-image JMX
- Severity: HIGH
- Inspects: application classes assignable to
javax.management.DynamicMBean(this also covers Model MBeans, sinceModelMBeanextendsDynamicMBean), other than classes based on the JDK'sStandardMBeanwrapper (StandardMBeanitself implementsDynamicMBean, so a naive assignability check would otherwise misflag the JDK's own supported "standard MBean via subclassing" pattern). - Fires when: a class implements or extends a dynamic/model MBean type. GraalVM's native-image JMX support only covers MXBeans and standard (interface-naming-convention) MBeans; dynamic and model MBeans define their management interface at run time, which is unsupported because there is no metadata registration that makes a dynamic or model MBean work in a native image.
- Recommendation: replace the dynamic/model MBean with a standard MBean (a
FooMBeaninterface plus aFooimplementation, orjavax.management.StandardMBeancomposition/subclassing) or an MXBean; both work with--enable-monitoring=jmxserver.