Add graalvm-native-images: Boot 4.1 + GraalVM CE for JDK 25, AOT processing,
the tracing agent, and a real reflection-collision trap Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EQNA6DJ9VgCtW6zhCE8Xud
This commit is contained in:
@@ -21,3 +21,8 @@ spring-batch-partitioning/data/
|
||||
# (maven-dependency-plugin's build-classpath goal) for the standalone MigrationCli
|
||||
# and LoadGenerator entry points - machine-specific, regenerated on every build
|
||||
db-migrations-expand-contract/cp.txt
|
||||
|
||||
# graalvm-native-images: raw native-image-agent output directory (the relevant
|
||||
# entries are copied by hand into src/main/resources/META-INF/native-image/...;
|
||||
# this raw directory is a build-time scratch artifact, not documentation)
|
||||
graalvm-native-images/agent-output/
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# GraalVM native images of a Spring Boot 4.1 application
|
||||
|
||||
Companion project for [**GraalVM Native Images for Spring Boot 4.1: AOT Processing, the Tracing
|
||||
Agent, and a Real Reflection Failure**](https://ankurm.com/building-native-images-of-spring-boot-applications-with-graalvm-a-step-by-step-guide/)
|
||||
on ankurm.com.
|
||||
|
||||
A small Spring Boot 4.1 web app, built and run three ways -- a plain `java -jar`, the same jar
|
||||
with Spring's AOT processing applied, and a GraalVM native image -- with every startup time,
|
||||
memory number, and failure message in this README and the linked post captured from a real run,
|
||||
not estimated.
|
||||
|
||||
## Versions
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Spring Boot | 4.1.1 |
|
||||
| JDK (JVM runs) | Temurin 25.0.4.1 |
|
||||
| GraalVM (native builds) | GraalVM Community Edition for JDK 25, build 25.0.2+10.1 |
|
||||
| `native-maven-plugin` | 1.1.8 (pinned by `spring-boot-starter-parent:4.1.1`'s dependency management; 1.1.14 is the latest on Maven Central as of this writing, but the parent's pinned version is what actually runs unless overridden) |
|
||||
|
||||
The previous version of this project and its post assumed Spring Boot 3.x, JDK 17, and GraalVM
|
||||
22.3 -- two major generations behind. See [docs/01-versions-and-setup.md](docs/01-versions-and-setup.md)
|
||||
for how each version claim here was checked.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# 1. Plain JVM run (any JDK 25 build)
|
||||
mvn -DskipTests package
|
||||
java -jar target/app.jar
|
||||
|
||||
# 2. Native image (needs a GraalVM distribution -- native-image ships bundled, no `gu install` step)
|
||||
export JAVA_HOME=/path/to/graalvm-25.0.2
|
||||
mvn -Pnative -DskipTests native:compile # NOT `mvn -Pnative package` -- see docs/02-building-the-image.md
|
||||
./target/graalvm-native-images
|
||||
```
|
||||
|
||||
## What's in this chapter
|
||||
|
||||
| Doc | Covers |
|
||||
|---|---|
|
||||
| [01-versions-and-setup.md](docs/01-versions-and-setup.md) | Verified version claims; why `gu install native-image` is obsolete |
|
||||
| [02-building-the-image.md](docs/02-building-the-image.md) | AOT processing's real generated output; why `-Pnative package` alone does not build a native image |
|
||||
| [03-the-reflection-trap.md](docs/03-the-reflection-trap.md) | A real `ClassNotFoundException` from a runtime-computed `Class.forName`, the tracing-agent fix, and the metadata-collision trap that fix runs into |
|
||||
| [04-honest-limits.md](docs/04-honest-limits.md) | What native images cost you, and where Project Leyden fits as the non-full-native alternative |
|
||||
|
||||
Every number quoted in the post is in [docs/output/](docs/output) as a trimmed, real transcript.
|
||||
|
||||
## Measured, not estimated
|
||||
|
||||
| | plain JVM | native image |
|
||||
|---|---|---|
|
||||
| Startup ("Started ... in") | 3.472 s | 0.091 s (0.104 s with reflection hints added) |
|
||||
| RSS at idle | ~190.8 MB | ~105.5 MB |
|
||||
| Artifact size | 22 MB jar (+ a JDK install) | 93 MB self-contained executable |
|
||||
| Build time | ordinary `mvn package` | 4m 14s–4m 19s on 2 vCPU / 8 GB |
|
||||
|
||||
Full transcripts: [docs/output/01-jvm-baseline.txt](docs/output/01-jvm-baseline.txt),
|
||||
[docs/output/02-native-build.txt](docs/output/02-native-build.txt),
|
||||
[docs/output/05-native-startup-fixed.txt](docs/output/05-native-startup-fixed.txt).
|
||||
@@ -0,0 +1,46 @@
|
||||
# 01 — Versions and setup
|
||||
|
||||
[Back to README →](../README.md) | [Next: 02 — building the image →](02-building-the-image.md)
|
||||
|
||||
## The version this project's own earlier post got wrong
|
||||
|
||||
The previous version of the linked post assumed Spring Boot 3.x, JDK 17, and "GraalVM 22.3" --
|
||||
installed the old way, via `sdk install java 22.3.r17-grl` followed by a separate
|
||||
`gu install native-image` step.
|
||||
|
||||
Checked directly rather than assumed:
|
||||
|
||||
- **GraalVM ships `native-image` bundled by default now.** Downloading GraalVM Community Edition
|
||||
for JDK 25 (build 25.0.2, verified by downloading it from
|
||||
`github.com/graalvm/graalvm-ce-builds/releases/download/jdk-25.0.2/...` and running
|
||||
`native-image --version`) and checking `$JAVA_HOME/bin/` shows `native-image`,
|
||||
`native-image-configure`, and `jnativescan` already present. The `gu install native-image` step
|
||||
the old post described was retired years ago; running it against a current GraalVM distribution
|
||||
fails because the `gu` tool itself was removed.
|
||||
- **The current official docs' local-build command is `mvn -Pnative native:compile`**, not
|
||||
`./mvnw clean package -Pnative` (what the old post used). Chapter 02 shows exactly why that
|
||||
distinction matters -- `package` alone silently does not invoke the GraalVM compiler at all in
|
||||
this verified setup.
|
||||
- **`spring-boot-starter-parent:4.1.1` already wires up `native-maven-plugin` with zero extra
|
||||
configuration.** The old post's manual `<profile>` block, hardcoded `<native-buildtools.version>`
|
||||
property, and explicit `<mainClass>` are all unnecessary now -- this project's
|
||||
[pom.xml](../pom.xml) needs only a bare `<profile><id>native</id>` with the plugin declared and
|
||||
no version, and `mvn help:effective-pom -Pnative` confirms Maven resolves it to version 1.1.8,
|
||||
which is what `spring-boot-starter-parent` pins via its own dependency management (1.1.14 is the
|
||||
newest release on Maven Central as of this writing, per `native-maven-plugin`'s
|
||||
`maven-metadata.xml`, but the parent's pinned version is what actually runs unless you override
|
||||
it).
|
||||
|
||||
## What this project pins, and why
|
||||
|
||||
| | Version | Checked how |
|
||||
|---|---|---|
|
||||
| Spring Boot | 4.1.1 | Matches the rest of this repository's other chapters |
|
||||
| JDK for JVM runs | Temurin 25.0.4.1 | `java -version` |
|
||||
| GraalVM for native builds | Community Edition for JDK 25, build 25.0.2+10.1 | Downloaded directly, `native-image --version` |
|
||||
| `native-maven-plugin` | 1.1.8 | `mvn help:effective-pom -Pnative`; Maven Central's `maven-metadata.xml` lists 1.1.14 as newest, confirming 1.1.8 is a deliberate, slightly older pin from Spring Boot's own dependency management, not the newest available |
|
||||
|
||||
A plain JDK (Temurin, in `$JDK25_HOME`) is enough for every JVM-mode measurement in this project.
|
||||
Only the native-image build itself needs the separate GraalVM distribution in `$GRAALVM_HOME`
|
||||
(see [scripts/run-all.sh](../scripts/run-all.sh)) -- and switching `JAVA_HOME` back and forth
|
||||
between the two is the only genuinely fiddly part of the whole setup.
|
||||
@@ -0,0 +1,53 @@
|
||||
# 02 — AOT processing, and why `package` alone doesn't build the image
|
||||
|
||||
[← Previous: 01 — versions and setup](01-versions-and-setup.md) | [Back to README →](../README.md) | [Next: 03 — the reflection trap →](03-the-reflection-trap.md)
|
||||
|
||||
## What Spring's AOT processing actually generates
|
||||
|
||||
`mvn spring-boot:process-aot` (bound to the `package` phase automatically, no profile needed) is
|
||||
what makes a Spring Boot native image possible at all -- it runs the application's startup logic
|
||||
once, at build time, and writes down what it learns as plain Java source instead of leaving it to
|
||||
be rediscovered by reflection at every future startup. On this project it produced, in
|
||||
`target/spring-aot/main/`:
|
||||
|
||||
- **95 generated `.java` source files** -- one `*__BeanDefinitions.java` per auto-configuration
|
||||
class and application bean, including
|
||||
[`HelloController__BeanDefinitions.java`](../src/main/java/com/ankurm/graalvmdemo/HelloController.java)
|
||||
and `ReportController__BeanDefinitions.java`, plus a
|
||||
`GraalvmDemoApplication__ApplicationContextInitializer.java` that builds the whole
|
||||
`ApplicationContext` as ordinary Java calls instead of classpath scanning.
|
||||
- **`META-INF/native-image/com.ankurm/graalvm-native-images/reachability-metadata.json`** -- the
|
||||
unified reflection/resource/proxy configuration format GraalVM now uses (a single file, not the
|
||||
older separate `reflect-config.json` / `resource-config.json` / `proxy-config.json` trio).
|
||||
- **`native-image.properties`** -- build arguments the plugin feeds straight to `native-image`.
|
||||
|
||||
## Why `mvn -Pnative package` alone does not produce a binary
|
||||
|
||||
Reflected in this exact setup, not assumed: running `mvn -Pnative -DskipTests package` completes
|
||||
in under nine seconds and its log contains `add-reachability-metadata`, `process-aot`, `jar`, and
|
||||
`repackage` -- and nothing else. Grepping that log for `native-image` or `Generating '` returns
|
||||
zero matches. `mvn help:effective-pom -Pnative` confirms why: the `native` profile this project
|
||||
(and a default Spring Initializr project) declares binds *no* execution of its own to any phase --
|
||||
it only adds the plugin so its own `add-reachability-metadata` goal, bound in the plugin's own
|
||||
metadata, participates in the build. The goal that actually invokes the GraalVM compiler,
|
||||
`native:compile`, has to be run explicitly:
|
||||
|
||||
```
|
||||
mvn -Pnative -DskipTests native:compile
|
||||
```
|
||||
|
||||
which matches Spring's own current documentation for local (non-Docker) native builds. The
|
||||
`./mvnw clean package -Pnative` command in tutorials and in the earlier version of this project's
|
||||
post does not do this -- confirmed by testing it directly, not by reading the plugin's changelog.
|
||||
Full transcripts of both runs, side by side:
|
||||
[docs/output/06-package-does-not-compile-native.txt](output/06-package-does-not-compile-native.txt)
|
||||
and [docs/output/02-native-build.txt](output/02-native-build.txt).
|
||||
|
||||
## The build itself, measured
|
||||
|
||||
On this project's 2 vCPU / 8 GB build machine, `native-image` analysis found 20,126 reachable
|
||||
types across 29,154 fields and 91,938 methods, registered 7,495 types for reflection, and finished
|
||||
in 4 minutes 14 seconds to 4 minutes 19 seconds across repeated runs, peaking at 4.36 GB resident
|
||||
memory during compilation. The resulting executable is 93 MB, versus a 22 MB jar that still needs
|
||||
a JRE installed alongside it to run at all. Full transcript:
|
||||
[docs/output/02-native-build.txt](output/02-native-build.txt).
|
||||
@@ -0,0 +1,77 @@
|
||||
# 03 — A real reflection failure, its fix, and the trap inside the fix
|
||||
|
||||
[← Previous: 02 — building the image](02-building-the-image.md) | [Back to README →](../README.md) | [Next: 04 — honest limits →](04-honest-limits.md)
|
||||
|
||||
## The trap
|
||||
|
||||
[`ReportController`](../src/main/java/com/ankurm/graalvmdemo/report/ReportController.java) builds
|
||||
a class name at request time from a query parameter and instantiates it reflectively --
|
||||
[`PlainTextReport`](../src/main/java/com/ankurm/graalvmdemo/report/PlainTextReport.java) or
|
||||
[`JsonReport`](../src/main/java/com/ankurm/graalvmdemo/report/JsonReport.java) -- the same shape
|
||||
as a plugin loaded by name from configuration, or a strategy resolved from a database column.
|
||||
Spring's AOT engine registers reflection metadata for everything it can see statically -- every
|
||||
`@RestController`, every bean -- but it has no way to know that a `format` query parameter can
|
||||
resolve to one of these two classes; nothing in the source connects them at build time.
|
||||
|
||||
On the plain JVM, this works exactly as written. On the first native build (no hints added),
|
||||
hitting the endpoint fails outright:
|
||||
|
||||
```
|
||||
REFLECTION-FAILED class=com.ankurm.graalvmdemo.report.PlainTextReport exception=java.lang.ClassNotFoundException message=com.ankurm.graalvmdemo.report.PlainTextReport
|
||||
```
|
||||
|
||||
Full transcript: [docs/output/03-native-startup-broken.txt](output/03-native-startup-broken.txt).
|
||||
Note it is `ClassNotFoundException`, not merely a reflection-access error -- GraalVM's closed-world
|
||||
build excluded the class from the binary entirely, because nothing reachable from `main()` proved
|
||||
it was needed.
|
||||
|
||||
## The fix: the tracing agent, run against the plain jar
|
||||
|
||||
`native-image-agent`, attached to a normal JVM run, watches real reflective calls and writes the
|
||||
same unified `reachability-metadata.json` format the AOT engine produces:
|
||||
|
||||
```
|
||||
java -agentlib:native-image-agent=config-output-dir=agent-output -jar target/app.jar
|
||||
curl "localhost:8083/report?format=plain"
|
||||
curl "localhost:8083/report?format=json"
|
||||
```
|
||||
|
||||
Full transcript: [docs/output/04-tracing-agent.txt](output/04-tracing-agent.txt). The agent adds
|
||||
real overhead to startup while attached -- 5.552 s here versus the 3.472 s baseline -- which is
|
||||
why it belongs in a one-off exploratory run or a dedicated test suite, not in production.
|
||||
|
||||
## The trap inside the fix
|
||||
|
||||
Copying the agent's generated entries for `PlainTextReport` and `JsonReport` into
|
||||
`src/main/resources/META-INF/native-image/com.ankurm/graalvm-native-images/reachability-metadata.json`
|
||||
-- the project's own Maven coordinates, which felt like the obviously correct place -- silently
|
||||
did nothing. Rebuilding and re-testing still failed with the identical `ClassNotFoundException`.
|
||||
The cause, confirmed by diffing the two files byte-for-byte: Spring's own AOT engine writes its
|
||||
generated metadata to that *exact same path*, and whichever copy the build processes last wins.
|
||||
`target/classes/META-INF/native-image/com.ankurm/graalvm-native-images/reachability-metadata.json`
|
||||
turned out to be Spring's 188 KB generated file, identical to the one in `target/spring-aot/`,
|
||||
with the two hand-added entries nowhere in it.
|
||||
|
||||
The fix is to put hand-written hints under a namespace that cannot collide with a real Maven
|
||||
coordinate -- this project uses
|
||||
[`src/main/resources/META-INF/native-image/com.ankurm.graalvmdemo/manual-hints/reachability-metadata.json`](../src/main/resources/META-INF/native-image/com.ankurm.graalvmdemo/manual-hints/reachability-metadata.json).
|
||||
GraalVM merges every `META-INF/native-image/**/reachability-metadata.json` it finds on the
|
||||
classpath regardless of what the intermediate folder names are -- they exist purely so different
|
||||
jars' metadata files don't collide with each other, which is exactly the property this fix needs.
|
||||
With that in place, the same two endpoints succeed:
|
||||
|
||||
```
|
||||
OK via reflection on com.ankurm.graalvmdemo.report.PlainTextReport: REPORT: quarterly numbers
|
||||
OK via reflection on com.ankurm.graalvmdemo.report.JsonReport: {"report":"quarterly numbers"}
|
||||
```
|
||||
|
||||
Full transcript: [docs/output/05-native-startup-fixed.txt](output/05-native-startup-fixed.txt).
|
||||
Startup and memory barely moved (0.104 s, ~105.7 MB RSS versus 0.091 s and ~105.5 MB without the
|
||||
fix) -- two extra reflectively-constructible classes are noise against the rest of the image.
|
||||
|
||||
- Never hand-write hints at your own project's exact `groupId`/`artifactId` path -- verify with
|
||||
`diff` against `target/spring-aot/main/resources/...` if a hint you added seems to have no
|
||||
effect.
|
||||
- `@RegisterReflectionForBinding` on the calling code is the alternative to a hand-written or
|
||||
agent-generated JSON file, and does not have this collision risk since it feeds Spring's own
|
||||
AOT-generated file rather than a second one.
|
||||
@@ -0,0 +1,84 @@
|
||||
# 04 — What native images cost you, and where Leyden fits
|
||||
|
||||
[← Previous: 03 — the reflection trap](03-the-reflection-trap.md) | [Back to README →](../README.md)
|
||||
|
||||
## The costs this chapter's numbers already show
|
||||
|
||||
Chapters 02 and 03 measured two of the real costs directly, so this chapter does not repeat them
|
||||
as claims -- it names them plainly and adds the ones the rest of this small project did not happen
|
||||
to hit:
|
||||
|
||||
- **Build time.** 4m14s–4m19s for a single-endpoint demo app on 2 vCPU / 8 GB
|
||||
([docs/output/02-native-build.txt](output/02-native-build.txt)). This scales with the size of
|
||||
the reachable call graph, not the size of your source -- a real Spring Boot application pulling
|
||||
in JPA, Kafka clients, and a dozen starters routinely takes native builds into double-digit
|
||||
minutes on CI hardware. That is minutes added to every single build in the pipeline, not a one-time
|
||||
setup cost.
|
||||
- **Binary size.** 93 MB versus a 22 MB jar
|
||||
([docs/output/02-native-build.txt](output/02-native-build.txt)) -- and the 22 MB jar still needs
|
||||
a JRE sitting next to it in the container image, so the size comparison is closer than it looks,
|
||||
but the native binary is a single self-contained file with nothing else to install, which matters
|
||||
more for container layer caching and cold-pull time than for raw megabytes.
|
||||
- **The closed-world assumption, beyond reflection.** This project demonstrated one shape of it --
|
||||
a class name computed at runtime -- but the same static-analysis boundary affects anything the
|
||||
build cannot prove is reachable from `main()`: JDK dynamic proxies and CGLIB proxies (Spring's
|
||||
own `@Transactional` and `@Async` machinery relies on these; `native-image` needs proxy classes
|
||||
named explicitly, the same way it needed the reflection hints in chapter 03), JNI calls into
|
||||
native libraries, and anything based on running a scripting engine or loading classes from bytes
|
||||
at runtime (Groovy, Nashorn-style engines, hand-rolled plugin loaders). None of that appears in
|
||||
this project's own source, so no transcript here demonstrates it directly -- naming it without a
|
||||
captured failure is the one claim in this chapter not backed by this project's own run, and it is
|
||||
flagged as such rather than presented as measured.
|
||||
- **Library compatibility is real but improving.** Spring Boot's own starters, and most of the
|
||||
library ecosystem tracking Spring Boot 4.1, ship reachability metadata now, which is why this
|
||||
demo's own Spring-managed beans needed zero hand-written hints -- only the application's own
|
||||
runtime-computed `Class.forName` did. A library with no shipped metadata and no metadata in the
|
||||
[GraalVM Reachability Metadata Repository](https://github.com/oracle/graalvm-reachability-metadata)
|
||||
is still a real risk to budget time for, but it is no longer the default outcome it was in 2022.
|
||||
|
||||
## Where Project Leyden fits
|
||||
|
||||
[Project Leyden](https://ankurm.com/project-leyden-explained-aot-compilation-and-smart-caching-to-finally-fix-javas-cold-start/)
|
||||
solves the same visible symptom -- slow JVM cold start -- from the opposite direction. GraalVM
|
||||
`native-image` replaces the JVM with a static, closed-world-analyzed binary; Leyden keeps the
|
||||
standard HotSpot JVM and instead caches what a *training run* already did -- class loading and
|
||||
linking (JEP 483), and now method compilation (JEP 484) -- into a `.jsa` archive that a later run
|
||||
loads with `-XX:SharedArchiveFile=app.jsa` or has generated for it automatically with
|
||||
`-XX:+AutoCreateSharedArchive`.
|
||||
|
||||
That difference in approach is also the difference in trade-offs, and it runs in the opposite
|
||||
direction from every cost in this chapter:
|
||||
|
||||
| | GraalVM native image (this project) | Project Leyden |
|
||||
|---|---|---|
|
||||
| Runtime | Substrate VM, no JVM | Standard HotSpot JVM |
|
||||
| Reflection / dynamic class loading | Must be declared ahead of time (chapter 03) | Works unmodified -- it is the same JVM |
|
||||
| Dynamic proxies (`@Transactional`, `@Async`) | Need explicit registration | Work unmodified, though Spring needs `-XX:+RecordDynamicProxyData` during training to capture proxy shapes for the cache |
|
||||
| Build/train step | 4m14s–4m19s `native-image` compile, every build | A training run producing a `.jsa`, not a full recompile |
|
||||
| Cold start (this project's measurement) | 0.091s–0.104s | Not measured in this project -- post 6035's own benchmarks put it well below plain-JVM startup but above native-image's near-instant figures |
|
||||
| Portability | Platform- and architecture-specific binary | Platform-specific archive, but the JAR itself stays portable |
|
||||
| Failure mode when assumptions don't hold | Build fails, or a `ClassNotFoundException` at runtime (chapter 03) | Archive validation (classpath fingerprint, JVM flags, module graph) fails closed -- it silently falls back to a normal cold JVM start rather than crashing |
|
||||
|
||||
That last row is the practical reason to know both exist rather than picking one forever: Leyden's
|
||||
failure mode is a slower start, recoverable by re-training; a native image's failure mode is a
|
||||
`ClassNotFoundException` in production if a reflective path was missed, recoverable only by adding
|
||||
the hint and rebuilding. A team unwilling to own the closed-world discipline this whole chapter
|
||||
describes, but still wanting materially faster starts than a cold JVM, has Leyden as a real
|
||||
middle option -- not a hypothetical one, now that JEP 483 and JEP 484 have shipped.
|
||||
|
||||
## Should you actually do this?
|
||||
|
||||
<blockquote style="border-left:4px solid #999;padding:0.5em 1em;margin:1em 0;background:#f7f7f7;">
|
||||
Native images earn their cost where startup and memory are the metric that matters most directly:
|
||||
serverless functions billed by cold-start latency, CLI tools, and horizontally-scaled services that
|
||||
restart often. They cost the most where the team is smallest relative to the surface area of
|
||||
reflection-heavy libraries in play -- a large Spring Boot monolith pulling in a wide, occasionally
|
||||
unmaintained dependency tree will spend real engineering time on reachability metadata that a
|
||||
Leyden-based approach, or simply a well-tuned JVM with CDS, would not have asked for at all.
|
||||
</blockquote>
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [GraalVM Reachability Metadata Repository](https://github.com/oracle/graalvm-reachability-metadata) -- the shared source of hints for common libraries, consulted automatically by the Maven plugin
|
||||
- [Project Leyden Explained](https://ankurm.com/project-leyden-explained-aot-compilation-and-smart-caching-to-finally-fix-javas-cold-start/) -- this project's sibling post, with its own detailed JEP 483/484 walkthrough and benchmark numbers
|
||||
- [Spring's official native-image documentation](https://docs.spring.io/spring-boot/how-to/native-image/developing-your-first-application.html) -- the current source of truth this project's chapter 02 was checked against
|
||||
@@ -0,0 +1,7 @@
|
||||
$ export JAVA_HOME=/opt/jdk25 && java -jar target/app.jar --server.port=8081
|
||||
Starting GraalvmDemoApplication v1.0.0 using Java 25.0.4.1 with PID 796 (/tmp/sbd-work/spring-boot-demo/graalvm-native-images/target/app.jar started by root in /tmp/sbd-work/spring-boot-demo/graalvm-native-images)
|
||||
Tomcat started on port 8081 (http) with context path '/'
|
||||
Started GraalvmDemoApplication in 3.472 seconds (process running for 4.041)
|
||||
|
||||
$ ps -o rss= -p 796
|
||||
195356
|
||||
@@ -0,0 +1,18 @@
|
||||
$ export JAVA_HOME=/opt/graalvm25 && mvn -Pnative -DskipTests native:compile
|
||||
[2/8] Performing analysis... [*****] (94.1s @ 2.35GB)
|
||||
20,126 types, 29,154 fields, and 91,938 methods found reachable
|
||||
7,495 types, 4,032 fields, and 16,651 methods registered for reflection
|
||||
68 types, 71 fields, and 58 methods registered for JNI access
|
||||
0 downcalls and 0 upcalls registered for foreign access
|
||||
4 native libraries: dl, pthread, rt, z
|
||||
[6/8] Compiling methods... [**********] (99.2s @ 2.18GB)
|
||||
30.4s (11.7% of total time) in 1838 GCs | Peak RSS: 4.36GB | CPU load: 1.87
|
||||
Build artifacts:
|
||||
/tmp/sbd-work/spring-boot-demo/graalvm-native-images/target/graalvm-native-images (executable)
|
||||
Finished generating 'graalvm-native-images' in 4m 19s.
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] Total time: 04:31 min
|
||||
|
||||
$ ls -lh target/graalvm-native-images target/app.jar
|
||||
-rwxr-xr-x 1 root root 93M Sep 20 10:35 target/graalvm-native-images
|
||||
-rw-r--r-- 1 root root 22M Sep 20 10:31 target/app.jar
|
||||
@@ -0,0 +1,13 @@
|
||||
$ ./target/graalvm-native-images --server.port=8082
|
||||
Starting AOT-processed GraalvmDemoApplication using Java 25.0.2 with PID 1234 (/tmp/sbd-work/spring-boot-demo/graalvm-native-images/target/graalvm-native-images started by root in /tmp/sbd-work/spring-boot-demo/graalvm-native-images)
|
||||
Tomcat started on port 8082 (http) with context path '/'
|
||||
Started GraalvmDemoApplication in 0.091 seconds (process running for 0.097)
|
||||
|
||||
$ ps -o rss= -p 1234
|
||||
107984
|
||||
|
||||
$ curl -s "localhost:8082/report?format=plain"
|
||||
REFLECTION-FAILED class=com.ankurm.graalvmdemo.report.PlainTextReport exception=java.lang.ClassNotFoundException message=com.ankurm.graalvmdemo.report.PlainTextReport
|
||||
|
||||
$ curl -s "localhost:8082/report?format=json"
|
||||
REFLECTION-FAILED class=com.ankurm.graalvmdemo.report.JsonReport exception=java.lang.ClassNotFoundException message=com.ankurm.graalvmdemo.report.JsonReport
|
||||
@@ -0,0 +1,17 @@
|
||||
$ /opt/graalvm25/bin/java -agentlib:native-image-agent=config-output-dir=agent-output -jar target/app.jar --server.port=8083
|
||||
Started GraalvmDemoApplication in 5.552 seconds (process running for 6.51)
|
||||
|
||||
$ curl -s localhost:8083/hello
|
||||
Hello from a GraalVM native image!
|
||||
$ curl -s "localhost:8083/report?format=plain"
|
||||
OK via reflection on com.ankurm.graalvmdemo.report.PlainTextReport: REPORT: quarterly numbers
|
||||
$ curl -s "localhost:8083/report?format=json"
|
||||
OK via reflection on com.ankurm.graalvmdemo.report.JsonReport: {"report":"quarterly numbers"}
|
||||
|
||||
$ grep -A4 '"com.ankurm.graalvmdemo.report.PlainTextReport"' agent-output/reachability-metadata.json
|
||||
{
|
||||
"type": "com.ankurm.graalvmdemo.report.PlainTextReport",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": [] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
$ ./target/graalvm-native-images --server.port=8085
|
||||
Started GraalvmDemoApplication in 0.104 seconds (process running for 0.111)
|
||||
|
||||
$ curl -s "localhost:8085/report?format=plain"
|
||||
OK via reflection on com.ankurm.graalvmdemo.report.PlainTextReport: REPORT: quarterly numbers
|
||||
$ curl -s "localhost:8085/report?format=json"
|
||||
OK via reflection on com.ankurm.graalvmdemo.report.JsonReport: {"report":"quarterly numbers"}
|
||||
|
||||
$ ps -o rss= -p <pid>
|
||||
108120
|
||||
@@ -0,0 +1,12 @@
|
||||
$ mvn -Pnative -DskipTests package
|
||||
[INFO] --- native:1.1.8:add-reachability-metadata (add-reachability-metadata) @ graalvm-native-images ---
|
||||
[INFO] --- resources:3.5.0:resources (default-resources) @ graalvm-native-images ---
|
||||
[INFO] --- compiler:3.15.0:compile (default-compile) @ graalvm-native-images ---
|
||||
[INFO] --- spring-boot:4.1.1:process-aot (process-aot) @ graalvm-native-images ---
|
||||
[INFO] --- jar:3.5.1:jar (default-jar) @ graalvm-native-images ---
|
||||
[INFO] --- spring-boot:4.1.1:repackage (repackage) @ graalvm-native-images ---
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] Total time: 8.791 s
|
||||
# No native-image invocation anywhere in this log -- confirmed by grepping the full log for
|
||||
# "native-image" and "Generating '": zero matches. Only "mvn -Pnative native:compile" (run
|
||||
# separately, see 02-native-build.txt) actually invokes the GraalVM compiler.
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>graalvm-native-images</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>graalvm-native-images</name>
|
||||
<description>GraalVM native images of a Spring Boot 4.1 application: AOT processing, the tracing agent, a real reflection failure and fix, and measured startup/memory numbers</description>
|
||||
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>app</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduces every measurement behind the ankurm.com GraalVM native images post.
|
||||
# Requires: JAVA_HOME pointed at a plain JDK 25 for the JVM steps, and a separate GraalVM
|
||||
# distribution (with the bundled native-image tool) for the native steps -- see docs/00-versions.md.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "== 1. plain JVM baseline =="
|
||||
"$JDK25_HOME/bin/java" -jar target/app.jar --server.port=8081 &
|
||||
JPID=$!
|
||||
sleep 5
|
||||
curl -s localhost:8081/hello; echo
|
||||
ps -o rss= -p $JPID
|
||||
kill $JPID
|
||||
|
||||
echo "== 2. build the native image (mvn -Pnative native:compile) =="
|
||||
mvn -Pnative -DskipTests native:compile
|
||||
|
||||
echo "== 3. run the native image, reflection trap included =="
|
||||
./target/graalvm-native-images --server.port=8082 &
|
||||
NPID=$!
|
||||
sleep 1
|
||||
curl -s "localhost:8082/report?format=plain"; echo
|
||||
ps -o rss= -p $NPID
|
||||
kill $NPID
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproduces the tracing-agent fix for the /report reflection trap: run the plain jar under the
|
||||
# GraalVM tracing agent, exercise both code paths, then rebuild the native image with the
|
||||
# generated hints merged in under a namespace that does NOT collide with Spring's own AOT output.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
rm -rf agent-output && mkdir -p agent-output
|
||||
"$GRAALVM_HOME/bin/java" -agentlib:native-image-agent=config-output-dir=agent-output \
|
||||
-jar target/app.jar --server.port=8083 &
|
||||
AGENT_PID=$!
|
||||
sleep 10
|
||||
curl -s "localhost:8083/report?format=plain"; echo
|
||||
curl -s "localhost:8083/report?format=json"; echo
|
||||
kill $AGENT_PID
|
||||
|
||||
echo "Generated hints for our own classes:"
|
||||
grep -A4 "com.ankurm.graalvmdemo.report" agent-output/reachability-metadata.json
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.ankurm.graalvmdemo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Companion app for the ankurm.com GraalVM native images post. Built three ways from the same
|
||||
* source: a plain {@code java -jar}, a Spring Boot AOT-processed JVM run, and a GraalVM native
|
||||
* image. See docs/02-building-the-image.md for the exact commands behind each measurement.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class GraalvmDemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GraalvmDemoApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ankurm.graalvmdemo;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* The baseline endpoint used for every startup-time and memory measurement in this chapter --
|
||||
* deliberately trivial, so the numbers reflect the framework and the runtime, not application
|
||||
* work. See docs/01-building-the-image.md.
|
||||
*/
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/hello")
|
||||
public String hello() {
|
||||
return "Hello from a GraalVM native image!";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ankurm.graalvmdemo.report;
|
||||
|
||||
public class JsonReport implements ReportFormat {
|
||||
@Override
|
||||
public String render(String message) {
|
||||
return "{\"report\":\"" + message + "\"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ankurm.graalvmdemo.report;
|
||||
|
||||
public class PlainTextReport implements ReportFormat {
|
||||
@Override
|
||||
public String render(String message) {
|
||||
return "REPORT: " + message;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.ankurm.graalvmdemo.report;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Loads a {@link ReportFormat} implementation by a class name built at request time from the
|
||||
* {@code format} query parameter -- a stand-in for the real-world "pluggable strategy resolved by
|
||||
* a string from configuration" pattern (a plugin name, a feature flag, a database column).
|
||||
* Nothing here gives GraalVM's build-time static analysis a way to know which classes this
|
||||
* reflective call can reach; see docs/02-the-reflection-trap.md for the real failure and its fix.
|
||||
*/
|
||||
@RestController
|
||||
public class ReportController {
|
||||
|
||||
@GetMapping("/report")
|
||||
public String report(@RequestParam(defaultValue = "plain") String format, @RequestParam(defaultValue = "quarterly numbers") String message) {
|
||||
String simpleName = "plain".equals(format) ? "PlainTextReport" : "JsonReport";
|
||||
String className = "com.ankurm.graalvmdemo.report." + simpleName;
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
ReportFormat instance = (ReportFormat) clazz.getDeclaredConstructor().newInstance();
|
||||
return "OK via reflection on " + className + ": " + instance.render(message);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return "REFLECTION-FAILED class=" + className + " exception=" + e.getClass().getName() + " message=" + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ankurm.graalvmdemo.report;
|
||||
|
||||
/**
|
||||
* Deliberately loaded by class name at runtime, never referenced by a Spring bean definition or a
|
||||
* static {@code new PlainTextReport()} anywhere in this codebase -- see docs/02-the-reflection-trap.md
|
||||
* for why that specific shape is what breaks under GraalVM's closed-world analysis.
|
||||
*/
|
||||
public interface ReportFormat {
|
||||
String render(String message);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"reflection": [
|
||||
{
|
||||
"type": "com.ankurm.graalvmdemo.report.PlainTextReport",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": [] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "com.ankurm.graalvmdemo.report.JsonReport",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
spring:
|
||||
application:
|
||||
name: graalvm-native-images
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health
|
||||
|
||||
logging:
|
||||
pattern:
|
||||
console: "%msg%n"
|
||||
Reference in New Issue
Block a user