Compare commits
24
Commits
b0c39a8d4c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
008211f2fa | ||
|
|
b9388053c4 | ||
|
|
4370ac7fdf | ||
|
|
97f946136b | ||
|
|
5e4d481c02 | ||
|
|
5851521420 | ||
|
|
f736108067 | ||
|
|
83d245e726 | ||
|
|
fd6d0fa1e7 | ||
|
|
ae681f85c4 | ||
|
|
804c33f0d5 | ||
|
|
b4b43b94bf | ||
|
|
e4f927d7af | ||
|
|
acd63e658b | ||
|
|
3e2029ab3a | ||
|
|
fa4205f631 | ||
|
|
a2cc7bbd08 | ||
|
|
2cbf31e7d5 | ||
|
|
e59ff029a1 | ||
|
|
d6b76c57db | ||
|
|
c418589251 | ||
|
|
d6a2e1a5f2 | ||
|
|
51f3ecd5da | ||
|
|
9825efa081 |
@@ -9,6 +9,7 @@ files.
|
|||||||
|
|
||||||
| Directory | Article | What it demonstrates |
|
| Directory | Article | What it demonstrates |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| [`hexagonal/`](hexagonal) | [Hexagonal Architecture (Ports and Adapters) in Spring Boot 4](https://ankurm.com/hexagonal-architecture-ports-and-adapters-spring-boot-4/) | a 4-module Maven reactor that makes "the core has no Spring dependency" something `mvn dependency:tree` proves rather than a naming convention, testing the core with zero Spring context, and a Spring Boot 4 test-slice-artifact split (`@DataJpaTest`, `@WebMvcTest`, `TestRestTemplate`) hit and fixed along the way |
|
||||||
| [`actuator-in-production/`](actuator-in-production) | [Spring Boot Actuator in Production](https://ankurm.com/spring-boot-actuator-production-endpoints-security-health-indicators/) | endpoint exposure defaults, securing Actuator, custom health indicators and how they hang |
|
| [`actuator-in-production/`](actuator-in-production) | [Spring Boot Actuator in Production](https://ankurm.com/spring-boot-actuator-production-endpoints-security-health-indicators/) | endpoint exposure defaults, securing Actuator, custom health indicators and how they hang |
|
||||||
| [`spring-boot-startup-time/`](spring-boot-startup-time) | [Why Your Spring Boot App Takes 8 Seconds to Start](spring-boot-startup-time/post/post.md) | `BufferingApplicationStartup`, JFR startup events, self time vs total time, the classpath-scan tax, the JDK 25 AOT cache |
|
| [`spring-boot-startup-time/`](spring-boot-startup-time) | [Why Your Spring Boot App Takes 8 Seconds to Start](spring-boot-startup-time/post/post.md) | `BufferingApplicationStartup`, JFR startup events, self time vs total time, the classpath-scan tax, the JDK 25 AOT cache |
|
||||||
| [`configuration-properties/`](configuration-properties) | [@ConfigurationProperties vs @Value in Spring Boot 4](https://ankurm.com/) | relaxed binding measured three ways, record constructor binding, validation, IDE metadata generation |
|
| [`configuration-properties/`](configuration-properties) | [@ConfigurationProperties vs @Value in Spring Boot 4](https://ankurm.com/) | relaxed binding measured three ways, record constructor binding, validation, IDE metadata generation |
|
||||||
@@ -31,6 +32,27 @@ files.
|
|||||||
| [`custom-validation/`](custom-validation) | [Custom Validation in Spring Boot: Beyond the Basics!](https://ankurm.com/custom-validation-in-spring-boot-beyond-the-basics/) | the `javax.validation` to `jakarta.validation` namespace fix Boot 3 already required, Jakarta Validation 3.1's record-validation clarification proven on both field- and class-level custom constraints, a record validation failure's empty 400 body by default, and what `spring.mvc.problemdetails.enabled` does and does not fix |
|
| [`custom-validation/`](custom-validation) | [Custom Validation in Spring Boot: Beyond the Basics!](https://ankurm.com/custom-validation-in-spring-boot-beyond-the-basics/) | the `javax.validation` to `jakarta.validation` namespace fix Boot 3 already required, Jakarta Validation 3.1's record-validation clarification proven on both field- and class-level custom constraints, a record validation failure's empty 400 body by default, and what `spring.mvc.problemdetails.enabled` does and does not fix |
|
||||||
| [`etag-caching/`](etag-caching) | [Mastering Cache Control with ETag in Spring Boot RESTful APIs](https://ankurm.com/etag-cache-control-rest-api-spring-boot/) | `spring-boot-starter-web`'s own POM now reading "deprecated in favor of spring-boot-starter-webmvc", `ShallowEtagHeaderFilter` and `WebRequest.checkNotModified()` re-verified unchanged on Spring Framework 7, deep cache vs shallow cache, and conditional `PUT` with `If-Match` as optimistic locking |
|
| [`etag-caching/`](etag-caching) | [Mastering Cache Control with ETag in Spring Boot RESTful APIs](https://ankurm.com/etag-cache-control-rest-api-spring-boot/) | `spring-boot-starter-web`'s own POM now reading "deprecated in favor of spring-boot-starter-webmvc", `ShallowEtagHeaderFilter` and `WebRequest.checkNotModified()` re-verified unchanged on Spring Framework 7, deep cache vs shallow cache, and conditional `PUT` with `If-Match` as optimistic locking |
|
||||||
| [`restclient-basic-auth/`](restclient-basic-auth) | [Spring Boot RestTemplate with Basic Auth: A Modern Guide](https://ankurm.com/spring-boot-resttemplate-with-basic-auth-a-modern-guide/) | RestClient with Basic Auth two ways against a real embedded server, `{noop}` passwords confirmed to emit no runtime warning at all, `spring-boot-starter-restclient` as its own required Boot 4 module, and the `RestClient.exchange()` trap covered in depth by the [RestTemplate to RestClient migration guide](https://ankurm.com/resttemplate-to-restclient-migration-guide/) |
|
| [`restclient-basic-auth/`](restclient-basic-auth) | [Spring Boot RestTemplate with Basic Auth: A Modern Guide](https://ankurm.com/spring-boot-resttemplate-with-basic-auth-a-modern-guide/) | RestClient with Basic Auth two ways against a real embedded server, `{noop}` passwords confirmed to emit no runtime warning at all, `spring-boot-starter-restclient` as its own required Boot 4 module, and the `RestClient.exchange()` trap covered in depth by the [RestTemplate to RestClient migration guide](https://ankurm.com/resttemplate-to-restclient-migration-guide/) |
|
||||||
|
| [`core-di/`](core-di) | [Dependency Injection in Spring Boot 4: Constructor vs Setter vs Field (and Why Field Injection Hurts)](https://ankurm.com/spring-boot-4-dependency-injection-constructor-setter-field/) and [@Autowired Explained: By-Type Resolution, @Qualifier, @Primary, ObjectProvider and List Injection](https://ankurm.com/spring-autowired-qualifier-primary-objectprovider-list-injection/) | one `OrderService` in three injection styles built with plain `new`, a field-injected dependency used in a constructor, circular dependencies in plain Spring vs Boot with the real failure report, `@Lazy` injecting a proxy, and the `@Autowired` resolution ladder read from Spring 7.0.9 bytecode (name match beats `@Priority`), plus the empty-collection trap that only bites field and setter injection |
|
||||||
|
| [`core-beans/`](core-beans) | [Spring Bean Scopes: Singleton, Prototype, Request, Session and the Prototype-in-Singleton Trap](https://ankurm.com/spring-bean-scopes-singleton-prototype-request-session-prototype-in-singleton-trap/) [Spring Bean Lifecycle in Boot 4: @PostConstruct, InitializingBean, SmartLifecycle and Shutdown Order](https://ankurm.com/spring-bean-lifecycle-postconstruct-smartlifecycle-shutdown-order/) and [Circular Dependencies in Spring Boot 4: Why Startup Fails and 4 Ways to Fix It](https://ankurm.com/spring-boot-4-circular-dependencies-startup-fails-4-fixes/) | instance counts per scope, five fixes for a prototype inside a singleton, request scope without a proxy failing at start-up, every lifecycle callback in order, `@PostConstruct` running before the proxy exists, `SmartLifecycle` phases measured, a `SmartLifecycle` at the default phase stopping before Tomcat has drained, and the circular-dependency start-up failure reproduced with four fixes |
|
||||||
|
| [`custom-starter/`](custom-starter) | [Write Your Own Spring Boot 4 Starter: Auto-Configuration, @Conditional and Properties](https://ankurm.com/write-your-own-spring-boot-4-starter-auto-configuration-conditional-properties/) | a starter split into an auto-configuration module and a starter, the `AutoConfiguration.imports` registration, `@ConditionalOn*` conditions, configuration metadata, and tests with `ApplicationContextRunner` |
|
||||||
|
| [`core-events/`](core-events) | [Spring Application Events: @EventListener, @TransactionalEventListener and Async Events](https://ankurm.com/spring-application-events-eventlistener-transactionaleventlistener-async/) | publishing and consuming events, listener ordering, conditional listeners, `AFTER_COMMIT` behaviour proven with a rollback, and async listeners |
|
||||||
|
| [`multi-datasource/`](multi-datasource) | [Multiple DataSources in Spring Boot 4 with Spring Data JPA](https://ankurm.com/multiple-datasources-spring-boot-4-spring-data-jpa/) | two H2 databases each with its own `DataSource`, Flyway, `EntityManagerFactory` and transaction manager, six ways of getting the wiring wrong, plain vs named `@Transactional`, and `ChainedTransactionManager` under a failing commit |
|
||||||
|
| [`i18n/`](i18n) | [Internationalization (i18n) in Spring Boot 4: MessageSource, LocaleResolver and Localized ProblemDetail](https://ankurm.com/spring-boot-4-internationalization-messagesource-localeresolver-problemdetail/) | English, Marathi and Hindi message bundles, `Accept-Language` vs a cookie resolver, apostrophes and Devanagari digits in `MessageFormat`, the JVM default locale as a hidden fallback, localized validation messages and `ProblemDetail` titles |
|
||||||
|
| [`aot-cache/`](aot-cache) | [Faster Spring Boot Startup with the JDK AOT Cache (JEP 514/515): Benchmarks vs CDS and Native](https://ankurm.com/spring-boot-jdk-aot-cache-jep-514-515-startup-benchmarks/) | the JDK 25 AOT cache trained two ways, against a plain JVM, AppCDS, Spring's own AOT output and a GraalVM native image (ten interleaved rounds each), first-request latency, 24 cases where the cache silently stops applying (jar, timestamp, JVM build, ZGC, compact headers), `-XX:AOTMode=on`, Docker layer arithmetic |
|
||||||
|
| [`kotlin/`](kotlin) | [Kotlin with Spring Boot 4.1: Null Safety, Coroutines, and What Java Developers Get Wrong](https://ankurm.com/) | Kotlin 2.3.21 (Boot 4.1.1's own managed version, not the latest GA), the real `@Configuration class may not be final` and `@Bean method must not be private or final` failures without the allopen/noarg `spring`/`jpa` compiler plugins, `suspend fun`/`Flow<T>` controller methods proven to still resume off a Tomcat thread under plain `spring-boot-starter-webmvc`, a JSpecify `@NullMarked` Java method that still NPEs at runtime because the contract isn't runtime-checked, and `TestRestTemplate` confirmed absent from Boot 4.1's `spring-boot-test` jar |
|
||||||
|
| [`testing-no-containers/`](testing-no-containers) | [Testing Spring Boot 4 Without Containers: WireMock, MockWebServer and @MockitoBean](https://ankurm.com/) | `@MockBean`'s total removal proven with a real `javac` failure, `@MockitoBean`/`@MockitoSpyBean` context-cache behavior proven with real `org.springframework.test.context.cache` DEBUG log lines, WireMock fault injection against a real `RestClient`, a real `NoClassDefFoundError` from mixing `wiremock` with `wiremock-jetty12` (fixed with `wiremock-standalone`), and MockWebServer's `SocketEffect.ShutdownConnection` for genuinely severing a connection mid-response |
|
||||||
|
| [`rest-test-client/`](rest-test-client) | [RestTestClient in Spring Framework 7: Testing REST APIs End-to-End (vs MockMvcTester)](https://ankurm.com/resttestclient-spring-framework-7-end-to-end-testing/) | one fluent `RestTestClient` API proven across `bindToController`, `bindTo(mockMvc)`, `bindToApplicationContext` and `bindToServer`; `RestTestClient`, `TestRestTemplate` and `@WebMvcTest` confirmed relocated into new Spring Boot 4 modules by unzipping the real jars; two distinct real failures for the two now-separate `@AutoConfigureRestTestClient`/`@AutoConfigureTestRestTemplate` annotations; and the JDK `HttpClient` Boot picks for `bindToServer` caught transparently decoding gzip and hiding `Content-Encoding`, verified against `curl` and against the older `SimpleClientHttpRequestFactory` |
|
||||||
|
| [`file-transfer/`](file-transfer) | [File Upload and Download in Spring Boot 4: Multipart, Streaming and S3 Presigned URLs](https://ankurm.com/file-upload-download-spring-boot-4-multipart-streaming-s3-presigned-urls/) | multipart limits enforced by Tomcat (and ignored by `MockMvc`), the client-controlled filename, `Range` downloads, a 200 MB download and 150 MB upload under `-Xmx64m`, and S3 presigned URLs checked against a local SigV4 verifier (not a real S3) |
|
||||||
|
|
||||||
|
`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n`, `aot-cache`, `kotlin`, `testing-no-containers`, `rest-test-client` and `hexagonal` are the exception: they have **no `docs/NN-topic.md` chapter folder**. Their deeper material lives in
|
||||||
|
collapsible sections inside the articles themselves. Where their captured output sits is not
|
||||||
|
uniform, and is worth checking per module rather than assuming: `core-di`, `core-beans`,
|
||||||
|
`custom-starter`, `core-events`, `multi-datasource`, `i18n` and `aot-cache` keep it in a
|
||||||
|
top-level `output/` directory (`hexagonal` is also the one multi-module reactor in this repo,
|
||||||
|
so its output sits one level deeper still, under each module: `core/output/`,
|
||||||
|
`persistence-adapter/output/`, and so on) — but `kotlin`, `testing-no-containers` and
|
||||||
|
`rest-test-client` still use `docs/output/`, same as the chaptered modules, just without the
|
||||||
|
`docs/NN-topic.md` chapters themselves.
|
||||||
|
|
||||||
Articles whose text is kept here rather than only on the blog have it under
|
Articles whose text is kept here rather than only on the blog have it under
|
||||||
`<directory>/post/` — `post.md` for the body and `meta.md` for the title, excerpt and
|
`<directory>/post/` — `post.md` for the body and `meta.md` for the title, excerpt and
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
target/
|
||||||
|
*.aot
|
||||||
|
*.aotconf
|
||||||
|
*.jsa
|
||||||
|
work/
|
||||||
|
work-run.log
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# aot-cache: the JDK 25 AOT cache on a Spring Boot 4.1 service
|
||||||
|
|
||||||
|
Companion project for **[Faster Spring Boot Startup with the JDK AOT Cache (JEP 514/515): Benchmarks vs CDS and Native](https://ankurm.com/spring-boot-jdk-aot-cache-jep-514-515-startup-benchmarks/)**
|
||||||
|
on [ankurm.com](https://ankurm.com). Everything the article quotes is a file in [`output/`](output), regenerated by
|
||||||
|
[`run.sh`](run.sh). The explanation lives in the article; this directory holds the code and the raw transcripts.
|
||||||
|
There is no `docs/` folder on purpose: the deeper material sits in collapsible sections of the article itself.
|
||||||
|
|
||||||
|
## The service
|
||||||
|
|
||||||
|
A small Spring Boot 4.1.1 web application, built to have the ingredients of a typical service and no more:
|
||||||
|
Spring MVC and Jackson (`POST /orders` with a JSON body), Bean Validation (a record with `@Email`, `@Pattern`,
|
||||||
|
`@Min`/`@Max`), the actuator health endpoint, and an in-memory store. No database. Sources are under
|
||||||
|
[`src/main/java/com/ankurm/aotcache/`](src/main/java/com/ankurm/aotcache).
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Spring Boot | 4.1.1 (Spring Framework 7.0.9, Tomcat 11.0.24, Jackson 3.1.5, Hibernate Validator 9.1.3) |
|
||||||
|
| JDK for the article | Temurin 25.0.4.1 (LTS) |
|
||||||
|
| JDK for the comparison | Temurin 27+35 |
|
||||||
|
| GraalVM (native image) | Community Edition 25.0.2+10.1 |
|
||||||
|
| Machine | 2 vCPU, 8 GB, Linux |
|
||||||
|
|
||||||
|
## What is here
|
||||||
|
|
||||||
|
| Script | What it does | Output |
|
||||||
|
|---|---|---|
|
||||||
|
| [`scripts/prepare.sh`](scripts/prepare.sh) | Builds the plain jar, a jar with Spring's own AOT output, a jar after a one-line change; extracts them | [`00-prepare.txt`](output/00-prepare.txt), [`00-environment.txt`](output/00-environment.txt) |
|
||||||
|
| [`scripts/native.sh`](scripts/native.sh) | Compiles the GraalVM native image | [`03-native-build.txt`](output/03-native-build.txt) |
|
||||||
|
| [`scripts/train.sh`](scripts/train.sh) | The training runs: AppCDS, AOT cache (context-only and with traffic, with and without Spring AOT, on JDK 27, under ZGC) | [`01-training.txt`](output/01-training.txt) |
|
||||||
|
| [`scripts/startup.sh`](scripts/startup.sh) | 12 configurations x 10 interleaved rounds: time to ready, Boot's own figure, first request, resident memory | [`02-startup.txt`](output/02-startup.txt) |
|
||||||
|
| [`scripts/mismatch.sh`](scripts/mismatch.sh) | 24 ways the cache and the run can disagree, and what the JVM says and does | [`04-mismatch.txt`](output/04-mismatch.txt) |
|
||||||
|
| [`scripts/layers.sh`](scripts/layers.sh) | Docker layer arithmetic without Docker: which layers change on a one-line edit, sizes, gzipped sizes | [`05-layers.txt`](output/05-layers.txt) |
|
||||||
|
| [`scripts/sample.sh`](scripts/sample.sh) | A transcript of the service's endpoints | [`06-sample-endpoints.txt`](output/06-sample-endpoints.txt) |
|
||||||
|
| [`docker/Dockerfile`](docker/Dockerfile) | A layered image with the cache trained in the final stage (**not built here**: no Docker daemon) | |
|
||||||
|
|
||||||
|
## Headline result
|
||||||
|
|
||||||
|
Median of ten runs, milliseconds from starting the process to the first `200` from `GET /ping`
|
||||||
|
([`output/02-startup.txt`](output/02-startup.txt)):
|
||||||
|
|
||||||
|
| Configuration | Ready | First `POST /orders` |
|
||||||
|
|---|---|---|
|
||||||
|
| Plain JVM, extracted jar | 3,920 | 173 |
|
||||||
|
| AppCDS | 2,583 | 164 |
|
||||||
|
| AOT cache, context-only training | 2,022 | 143 |
|
||||||
|
| AOT cache, training with traffic | 1,949 | 68 |
|
||||||
|
| AOT cache + Spring AOT, training with traffic | 1,436 | 89 |
|
||||||
|
| GraalVM native image | 133 | 5.3 |
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
|
||||||
|
Needs JDK 25, JDK 27, a GraalVM for JDK 25 and Maven; `scripts/env.sh` shows where they are looked up and each can be
|
||||||
|
overridden with `JDK25`, `JDK27` and `GRAALVM`. The measurement scripts assume Linux and `curl`, and need the machine to
|
||||||
|
themselves.
|
||||||
|
|
||||||
|
```console
|
||||||
|
cd aot-cache
|
||||||
|
./run.sh # about 35 minutes; the native build is 5 of them
|
||||||
|
ROUNDS=3 ./run.sh # a quick pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
* One small service on one 2-CPU virtual machine. The ratios are what to take away, not the milliseconds.
|
||||||
|
* No database and no JPA, so the service is smaller than most real ones; a larger classpath usually gains more from the cache.
|
||||||
|
* JEP 515 (method profiles) is part of the cache on JDK 25 and cannot be switched off separately, so its own contribution was not isolated.
|
||||||
|
* The Docker image was not built; the layer figures come from extracting the jar's layers and comparing files.
|
||||||
|
* `openjdk.org/jeps` returned HTTP 403 to the tooling used, so JEP claims come from running the JDKs and from secondary sources named in the article.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# NOT BUILT OR RUN while writing the article: the sandbox has no Docker daemon. The commands inside it are the ones
|
||||||
|
# scripts/train.sh and scripts/mismatch.sh ran (extract, AOTCacheOutput, AOTCache); the layer arithmetic is in
|
||||||
|
# output/05-layers.txt. Treat the file as a starting point and measure it in your own registry.
|
||||||
|
#
|
||||||
|
# Build: mvn -DskipTests package && docker build -f docker/Dockerfile -t aot-cache .
|
||||||
|
|
||||||
|
FROM eclipse-temurin:25-jdk AS extract
|
||||||
|
WORKDIR /build
|
||||||
|
COPY target/app.jar app.jar
|
||||||
|
RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted
|
||||||
|
|
||||||
|
# The final image is also where the cache is trained: the cache records the exact JVM build and the exact
|
||||||
|
# jar files (size and modification time), so it must be created by the JVM that will use it, after the last COPY.
|
||||||
|
FROM eclipse-temurin:25-jre
|
||||||
|
WORKDIR /application
|
||||||
|
COPY --from=extract /build/extracted/dependencies/ ./
|
||||||
|
COPY --from=extract /build/extracted/spring-boot-loader/ ./
|
||||||
|
COPY --from=extract /build/extracted/snapshot-dependencies/ ./
|
||||||
|
COPY --from=extract /build/extracted/application/ ./
|
||||||
|
RUN java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar app.jar
|
||||||
|
ENTRYPOINT ["java", "-XX:AOTCache=app.aot", "-jar", "app.jar"]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# machine
|
||||||
|
cpus: 2 memory: 8032 MB kernel: 6.18.44-fc-v37
|
||||||
|
|
||||||
|
# JDK 25 (the JVM the article is about)
|
||||||
|
openjdk version "25.0.4.1" 2026-08-18 LTS
|
||||||
|
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
|
||||||
|
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
|
||||||
|
|
||||||
|
# JDK 27 (the comparison)
|
||||||
|
openjdk version "27" 2026-09-15
|
||||||
|
OpenJDK Runtime Environment Temurin-27+35 (build 27+35)
|
||||||
|
OpenJDK 64-Bit Server VM Temurin-27+35 (build 27+35, mixed mode, sharing)
|
||||||
|
|
||||||
|
# GraalVM (native image)
|
||||||
|
native-image 25.0.2 2026-01-20
|
||||||
|
GraalVM Runtime Environment GraalVM CE 25.0.2+10.1 (build 25.0.2+10-jvmci-b01)
|
||||||
|
|
||||||
|
# libraries
|
||||||
|
hibernate-validator-9.1.3.Final
|
||||||
|
jackson-databind-3.1.5
|
||||||
|
spring-boot-4.1.1
|
||||||
|
spring-core-7.0.9
|
||||||
|
spring-webmvc-7.0.9
|
||||||
|
tomcat-embed-core-11.0.24
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
== build: plain jar (mvn package)
|
||||||
|
== build: jar with Spring AOT output (mvn -Pnative package; this does NOT compile a native image)
|
||||||
|
initializers in plain jar: 0
|
||||||
|
initializers in Spring AOT jar: 1
|
||||||
|
== extract both jars (java -Djarmode=tools -jar app.jar extract)
|
||||||
|
/home/claude/spring-boot-demo/aot-cache/work/ext:
|
||||||
|
app.jar
|
||||||
|
lib
|
||||||
|
|
||||||
|
/home/claude/spring-boot-demo/aot-cache/work/ext-springaot:
|
||||||
|
app.jar
|
||||||
|
lib
|
||||||
|
== build: the same service after a one-line change (the unit price in OrderService), for the mismatch and layer checks
|
||||||
|
edited: 20: BigDecimal unit = BigDecimal.valueOf(5_99, 2);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# training runs (JDK 25.0.4.1 unless noted); wall time includes JVM start, context start and cache creation
|
||||||
|
AppCDS (ArchiveClassesAtExit) 4741 ms appcds.jsa (38 MB)
|
||||||
|
AOT cache, fat jar, context-only 8219 ms fat.aot (56 MB)
|
||||||
|
AOT cache, context-only training 7916 ms refresh.aot (59 MB)
|
||||||
|
AOT cache, traffic training 10157 ms traffic.aot (66 MB)
|
||||||
|
AOT cache + Spring AOT, context-only 7164 ms springaot.aot (57 MB)
|
||||||
|
AOT cache + Spring AOT, traffic 8733 ms springaot-traffic.aot (64 MB)
|
||||||
|
AOT cache on JDK 27, context-only 9292 ms refresh27.aot (62 MB)
|
||||||
|
AOT cache on JDK 27, trained under ZGC 9426 ms zgc27.aot (62 MB)
|
||||||
|
|
||||||
|
# what the JVM said at warning level while writing the context-only AOT cache (count, then the distinct kinds)
|
||||||
|
warning lines: 122
|
||||||
|
96 [warning][aot] Skipping <class>: Unsupported location
|
||||||
|
4 [warning][aot] Skipping <class>: Unlinked class not supported by AOTClassLinking
|
||||||
|
4 [warning][aot] Skipping <class>: Failed verification
|
||||||
|
3 [warning][aot] Skipping <class>: JFR event class
|
||||||
|
2 [warning][aot] Skipping <class>: Not in loaded state
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# 12 configurations x 10 rounds; one unrecorded warm-up round first (so cache files are in the page cache)
|
||||||
|
|
||||||
|
== medians of 10 runs (min..max in brackets), milliseconds; rss in MB
|
||||||
|
configuration ready (exec->200) Boot 'Started in' 1st POST /orders next 50 (median) RSS
|
||||||
|
1 jvm-fat-jar 4630.5 [4281..4931] 3683.5 [3455..3954] 204.0 [166.6..230.7] 4.5 194.0
|
||||||
|
2 jvm-extracted 3919.5 [3486..4018] 3242.0 [2890..3400] 173.3 [146.6..191.7] 4.8 189.0
|
||||||
|
3 jvm+spring-aot 3162.0 [2991..3500] 2552.0 [2419..2907] 201.8 [165.3..254.3] 4.5 183.0
|
||||||
|
4 appcds 2582.5 [2436..2730] 2181.0 [2022..2330] 164.3 [133.9..170.2] 4.8 195.0
|
||||||
|
5 aot-cache fat-jar 3247.0 [3096..3507] 2553.5 [2432..2787] 156.6 [144.9..186.6] 4.7 235.0
|
||||||
|
6 aot-cache context-only 2021.5 [1948..2253] 1568.0 [1495..1785] 142.8 [116.1..160.0] 4.8 218.0
|
||||||
|
7 aot-cache traffic 1948.5 [1777..2142] 1623.5 [1406..1775] 67.7 [47.5..101.8] 4.5 198.5
|
||||||
|
8 aot-cache+spring-aot 1588.5 [1482..1674] 1120.5 [1030..1218] 146.6 [131.4..163.9] 4.5 188.0
|
||||||
|
9 aot-cache+spring-aot traffic 1436.0 [1372..1651] 1069.0 [1015..1280] 89.3 [65.9..113.0] 4.2 184.0
|
||||||
|
10 jdk27 plain 3779.5 [3595..4205] 3122.5 [2919..3408] 164.8 [139.9..176.8] 4.6 234.0
|
||||||
|
11 jdk27 aot-cache 1983.0 [1840..2181] 1571.5 [1444..1763] 138.2 [117.9..191.0] 4.2 195.5
|
||||||
|
12 native image 133.0 [127..171] 101.5 [90..133] 5.3 [4.6..6.0] 1.0 120.0
|
||||||
|
|
||||||
|
== every run
|
||||||
|
1 jvm-fat-jar 1 ready=4407 started=3460 first=201.9 second=6.4 next50=3.2 rss=186
|
||||||
|
2 jvm-extracted 1 ready=3486 started=2890 first=187.6 second=7.4 next50=4.0 rss=189
|
||||||
|
3 jvm+spring-aot 1 ready=3111 started=2495 first=203.0 second=8.9 next50=4.5 rss=189
|
||||||
|
4 appcds 1 ready=2535 started=2133 first=166.6 second=12.8 next50=4.8 rss=190
|
||||||
|
5 aot-cache fat-jar 1 ready=3096 started=2432 first=157.2 second=11.4 next50=4.3 rss=234
|
||||||
|
6 aot-cache context-only 1 ready=1948 started=1555 first=133.0 second=6.0 next50=3.4 rss=211
|
||||||
|
7 aot-cache traffic 1 ready=1984 started=1642 first=47.5 second=5.5 next50=4.5 rss=200
|
||||||
|
8 aot-cache+spring-aot 1 ready=1482 started=1030 first=153.1 second=7.2 next50=4.4 rss=189
|
||||||
|
9 aot-cache+spring-aot traffic 1 ready=1372 started=1057 first=73.2 second=11.0 next50=3.5 rss=184
|
||||||
|
10 jdk27 plain 1 ready=3595 started=2919 first=165.6 second=13.0 next50=4.5 rss=224
|
||||||
|
11 jdk27 aot-cache 1 ready=1890 started=1517 first=135.2 second=7.1 next50=3.4 rss=195
|
||||||
|
12 native image 1 ready=127 started=90 first=4.6 second=0.9 next50=0.7 rss=120
|
||||||
|
1 jvm-fat-jar 2 ready=4495 started=3692 first=166.6 second=20.1 next50=4.2 rss=195
|
||||||
|
2 jvm-extracted 2 ready=3725 started=3117 first=173.6 second=7.5 next50=4.1 rss=191
|
||||||
|
3 jvm+spring-aot 2 ready=3121 started=2522 first=199.1 second=10.0 next50=4.1 rss=188
|
||||||
|
4 appcds 2 ready=2496 started=2104 first=166.9 second=8.3 next50=4.2 rss=200
|
||||||
|
5 aot-cache fat-jar 2 ready=3250 started=2579 first=186.6 second=17.7 next50=4.6 rss=239
|
||||||
|
6 aot-cache context-only 2 ready=2029 started=1568 first=140.9 second=16.2 next50=3.5 rss=218
|
||||||
|
7 aot-cache traffic 2 ready=1840 started=1506 first=76.5 second=9.3 next50=3.4 rss=200
|
||||||
|
8 aot-cache+spring-aot 2 ready=1550 started=1070 first=131.4 second=14.7 next50=5.0 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 2 ready=1444 started=1070 first=78.5 second=5.8 next50=4.2 rss=185
|
||||||
|
10 jdk27 plain 2 ready=3767 started=3121 first=164.1 second=8.4 next50=3.9 rss=219
|
||||||
|
11 jdk27 aot-cache 2 ready=1844 started=1444 first=124.8 second=6.0 next50=4.2 rss=196
|
||||||
|
12 native image 2 ready=171 started=133 first=5.6 second=1.4 next50=1.0 rss=120
|
||||||
|
1 jvm-fat-jar 3 ready=4281 started=3455 first=188.9 second=7.0 next50=4.1 rss=187
|
||||||
|
2 jvm-extracted 3 ready=3944 started=3315 first=163.8 second=8.1 next50=4.0 rss=191
|
||||||
|
3 jvm+spring-aot 3 ready=2991 started=2419 first=254.3 second=9.6 next50=4.4 rss=177
|
||||||
|
4 appcds 3 ready=2436 started=2049 first=170.2 second=7.7 next50=4.5 rss=198
|
||||||
|
5 aot-cache fat-jar 3 ready=3127 started=2478 first=156.1 second=8.6 next50=4.4 rss=231
|
||||||
|
6 aot-cache context-only 3 ready=2020 started=1601 first=149.9 second=12.2 next50=4.7 rss=219
|
||||||
|
7 aot-cache traffic 3 ready=1777 started=1406 first=65.2 second=6.2 next50=4.2 rss=200
|
||||||
|
8 aot-cache+spring-aot 3 ready=1547 started=1066 first=134.8 second=6.7 next50=5.5 rss=190
|
||||||
|
9 aot-cache+spring-aot traffic 3 ready=1383 started=1015 first=84.9 second=6.3 next50=4.7 rss=184
|
||||||
|
10 jdk27 plain 3 ready=3704 started=3067 first=139.9 second=7.2 next50=4.4 rss=237
|
||||||
|
11 jdk27 aot-cache 3 ready=1840 started=1494 first=125.0 second=9.8 next50=4.1 rss=196
|
||||||
|
12 native image 3 ready=157 started=131 first=5.6 second=1.2 next50=0.9 rss=120
|
||||||
|
1 jvm-fat-jar 4 ready=4665 started=3675 first=190.9 second=7.0 next50=4.6 rss=196
|
||||||
|
2 jvm-extracted 4 ready=3837 started=3188 first=191.7 second=7.7 next50=5.2 rss=190
|
||||||
|
3 jvm+spring-aot 4 ready=3205 started=2568 first=165.3 second=12.6 next50=4.0 rss=177
|
||||||
|
4 appcds 4 ready=2450 started=2022 first=168.7 second=11.6 next50=3.7 rss=193
|
||||||
|
5 aot-cache fat-jar 4 ready=3244 started=2644 first=151.2 second=7.2 next50=4.9 rss=229
|
||||||
|
6 aot-cache context-only 4 ready=2012 started=1495 first=126.3 second=11.3 next50=4.0 rss=205
|
||||||
|
7 aot-cache traffic 4 ready=1899 started=1537 first=70.1 second=12.4 next50=5.5 rss=186
|
||||||
|
8 aot-cache+spring-aot 4 ready=1644 started=1207 first=163.0 second=13.2 next50=4.5 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 4 ready=1472 started=1144 first=65.9 second=10.6 next50=4.2 rss=184
|
||||||
|
10 jdk27 plain 4 ready=3712 started=3060 first=176.8 second=7.3 next50=4.7 rss=236
|
||||||
|
11 jdk27 aot-cache 4 ready=1988 started=1608 first=147.4 second=10.8 next50=4.0 rss=195
|
||||||
|
12 native image 4 ready=127 started=94 first=5.0 second=1.1 next50=1.0 rss=120
|
||||||
|
1 jvm-fat-jar 5 ready=4668 started=3654 first=217.1 second=7.7 next50=4.5 rss=203
|
||||||
|
2 jvm-extracted 5 ready=4018 started=3400 first=180.4 second=10.5 next50=5.2 rss=196
|
||||||
|
3 jvm+spring-aot 5 ready=3500 started=2907 first=177.4 second=12.6 next50=4.5 rss=186
|
||||||
|
4 appcds 5 ready=2690 started=2267 first=133.9 second=9.2 next50=4.5 rss=197
|
||||||
|
5 aot-cache fat-jar 5 ready=3169 started=2456 first=174.6 second=8.8 next50=5.1 rss=234
|
||||||
|
6 aot-cache context-only 5 ready=2253 started=1785 first=144.7 second=9.3 next50=5.8 rss=218
|
||||||
|
7 aot-cache traffic 5 ready=1939 started=1622 first=101.8 second=7.7 next50=4.5 rss=194
|
||||||
|
8 aot-cache+spring-aot 5 ready=1591 started=1143 first=142.9 second=10.1 next50=4.3 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 5 ready=1461 started=1126 first=96.8 second=6.1 next50=4.0 rss=184
|
||||||
|
10 jdk27 plain 5 ready=3709 started=3007 first=174.4 second=8.6 next50=5.0 rss=230
|
||||||
|
11 jdk27 aot-cache 5 ready=1987 started=1589 first=191.0 second=6.7 next50=4.5 rss=195
|
||||||
|
12 native image 5 ready=136 started=96 first=6.0 second=1.0 next50=1.0 rss=119
|
||||||
|
1 jvm-fat-jar 6 ready=4524 started=3575 first=196.3 second=8.5 next50=4.4 rss=195
|
||||||
|
2 jvm-extracted 6 ready=3986 started=3300 first=150.2 second=10.1 next50=4.8 rss=176
|
||||||
|
3 jvm+spring-aot 6 ready=3108 started=2560 first=217.2 second=13.3 next50=4.3 rss=177
|
||||||
|
4 appcds 6 ready=2730 started=2330 first=152.3 second=7.5 next50=5.0 rss=197
|
||||||
|
5 aot-cache fat-jar 6 ready=3399 started=2698 first=154.3 second=9.4 next50=5.2 rss=238
|
||||||
|
6 aot-cache context-only 6 ready=2211 started=1697 first=151.7 second=14.4 next50=5.7 rss=218
|
||||||
|
7 aot-cache traffic 6 ready=1872 started=1542 first=61.0 second=8.4 next50=5.2 rss=198
|
||||||
|
8 aot-cache+spring-aot 6 ready=1624 started=1150 first=135.1 second=12.4 next50=5.7 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 6 ready=1380 started=1051 first=107.5 second=11.7 next50=4.8 rss=185
|
||||||
|
10 jdk27 plain 6 ready=4103 started=3408 first=168.7 second=7.7 next50=5.1 rss=249
|
||||||
|
11 jdk27 aot-cache 6 ready=1961 started=1570 first=141.3 second=13.2 next50=5.0 rss=197
|
||||||
|
12 native image 6 ready=131 started=105 first=5.0 second=2.7 next50=1.2 rss=119
|
||||||
|
1 jvm-fat-jar 7 ready=4787 started=3913 first=224.4 second=10.0 next50=4.7 rss=197
|
||||||
|
2 jvm-extracted 7 ready=3899 started=3237 first=165.2 second=8.8 next50=5.4 rss=189
|
||||||
|
3 jvm+spring-aot 7 ready=3294 started=2623 first=172.8 second=8.7 next50=4.8 rss=184
|
||||||
|
4 appcds 7 ready=2592 started=2172 first=162.1 second=9.6 next50=4.8 rss=205
|
||||||
|
5 aot-cache fat-jar 7 ready=3347 started=2528 first=176.4 second=7.8 next50=4.8 rss=236
|
||||||
|
6 aot-cache context-only 7 ready=1989 started=1567 first=116.1 second=7.3 next50=5.5 rss=218
|
||||||
|
7 aot-cache traffic 7 ready=2111 started=1771 first=100.5 second=13.5 next50=4.5 rss=200
|
||||||
|
8 aot-cache+spring-aot 7 ready=1597 started=1147 first=152.7 second=10.4 next50=3.9 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 7 ready=1651 started=1280 first=85.4 second=9.1 next50=5.2 rss=185
|
||||||
|
10 jdk27 plain 7 ready=3825 started=3124 first=166.7 second=8.9 next50=4.5 rss=240
|
||||||
|
11 jdk27 aot-cache 7 ready=2033 started=1690 first=153.9 second=7.3 next50=5.5 rss=195
|
||||||
|
12 native image 7 ready=132 started=100 first=4.9 second=1.3 next50=1.2 rss=120
|
||||||
|
1 jvm-fat-jar 8 ready=4608 started=3729 first=211.0 second=8.0 next50=5.0 rss=192
|
||||||
|
2 jvm-extracted 8 ready=4008 started=3345 first=180.4 second=8.2 next50=4.5 rss=181
|
||||||
|
3 jvm+spring-aot 8 ready=3095 started=2479 first=214.1 second=8.6 next50=5.0 rss=177
|
||||||
|
4 appcds 8 ready=2573 started=2199 first=161.4 second=8.6 next50=5.0 rss=192
|
||||||
|
5 aot-cache fat-jar 8 ready=3177 started=2462 first=152.9 second=7.7 next50=4.6 rss=230
|
||||||
|
6 aot-cache context-only 8 ready=1981 started=1528 first=160.0 second=37.6 next50=5.0 rss=218
|
||||||
|
7 aot-cache traffic 8 ready=2142 started=1775 first=62.3 second=8.7 next50=4.1 rss=187
|
||||||
|
8 aot-cache+spring-aot 8 ready=1586 started=1098 first=163.9 second=12.0 next50=5.3 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 8 ready=1412 started=1047 first=93.3 second=9.0 next50=5.0 rss=184
|
||||||
|
10 jdk27 plain 8 ready=3792 started=3125 first=152.4 second=11.5 next50=5.1 rss=235
|
||||||
|
11 jdk27 aot-cache 8 ready=1979 started=1529 first=122.5 second=6.7 next50=3.8 rss=195
|
||||||
|
12 native image 8 ready=166 started=117 first=5.3 second=1.5 next50=1.1 rss=119
|
||||||
|
1 jvm-fat-jar 9 ready=4653 started=3760 first=230.7 second=10.4 next50=4.2 rss=193
|
||||||
|
2 jvm-extracted 9 ready=3728 started=3060 first=173.0 second=8.9 next50=4.8 rss=182
|
||||||
|
3 jvm+spring-aot 9 ready=3266 started=2544 first=200.6 second=10.7 next50=5.0 rss=182
|
||||||
|
4 appcds 9 ready=2596 started=2244 first=136.5 second=7.9 next50=5.2 rss=192
|
||||||
|
5 aot-cache fat-jar 9 ready=3507 started=2787 first=162.2 second=8.0 next50=4.8 rss=242
|
||||||
|
6 aot-cache context-only 9 ready=2152 started=1720 first=155.7 second=12.1 next50=4.8 rss=219
|
||||||
|
7 aot-cache traffic 9 ready=2088 started=1731 first=58.5 second=11.4 next50=4.0 rss=198
|
||||||
|
8 aot-cache+spring-aot 9 ready=1674 started=1218 first=137.0 second=11.3 next50=4.2 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 9 ready=1428 started=1068 first=113.0 second=10.8 next50=4.1 rss=184
|
||||||
|
10 jdk27 plain 9 ready=4205 started=3396 first=157.9 second=10.0 next50=4.7 rss=224
|
||||||
|
11 jdk27 aot-cache 9 ready=2181 started=1763 first=117.9 second=6.7 next50=4.0 rss=196
|
||||||
|
12 native image 9 ready=133 started=98 first=5.4 second=1.4 next50=1.1 rss=120
|
||||||
|
1 jvm-fat-jar 10 ready=4931 started=3954 first=206.1 second=8.8 next50=4.8 rss=193
|
||||||
|
2 jvm-extracted 10 ready=3940 started=3247 first=146.6 second=12.8 next50=4.8 rss=185
|
||||||
|
3 jvm+spring-aot 10 ready=3203 started=2587 first=206.7 second=8.9 next50=4.6 rss=187
|
||||||
|
4 appcds 10 ready=2607 started=2190 first=169.4 second=7.8 next50=5.2 rss=193
|
||||||
|
5 aot-cache fat-jar 10 ready=3495 started=2699 first=144.9 second=8.1 next50=3.7 rss=241
|
||||||
|
6 aot-cache context-only 10 ready=2023 started=1568 first=134.5 second=32.5 next50=4.5 rss=218
|
||||||
|
7 aot-cache traffic 10 ready=1958 started=1625 first=74.3 second=6.1 next50=5.1 rss=199
|
||||||
|
8 aot-cache+spring-aot 10 ready=1556 started=1095 first=150.2 second=9.2 next50=4.3 rss=188
|
||||||
|
9 aot-cache+spring-aot traffic 10 ready=1575 started=1213 first=107.0 second=6.5 next50=4.0 rss=184
|
||||||
|
10 jdk27 plain 10 ready=4010 started=3356 first=156.0 second=7.2 next50=4.4 rss=233
|
||||||
|
11 jdk27 aot-cache 10 ready=2016 started=1573 first=171.6 second=7.8 next50=4.5 rss=196
|
||||||
|
12 native image 10 ready=133 started=103 first=5.4 second=1.2 next50=1.0 rss=120
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# GraalVM: native-image 25.0.2 2026-01-20
|
||||||
|
21,929 types, 31,331 fields, and 99,355 methods found reachable
|
||||||
|
36.0s (11.7% of total time) in 2209 GCs | Peak RSS: 4.70GB | CPU load: 1.86
|
||||||
|
Finished generating 'aot-cache' in 5m 6s.
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 05:18 min
|
||||||
|
/home/claude/spring-boot-demo/aot-cache/work/aot-cache-native 99 MB
|
||||||
|
/home/claude/spring-boot-demo/aot-cache/work/app.jar 22 MB
|
||||||
|
build wall time: 320 s
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# cache: /home/claude/spring-boot-demo/aot-cache/work/refresh.aot, trained on JDK 25.0.4.1, extracted layout, default GC (G1), 2 CPUs
|
||||||
|
|
||||||
|
== control: the same JVM, the same jar, the same path
|
||||||
|
result: started, ready after 1987 ms (Started AotCacheApplication in 1.538 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== the application jar was rebuilt after a one-line code change, default AOTMode=auto
|
||||||
|
result: started, ready after 4124 ms (Started AotCacheApplication in 3.453 seconds)
|
||||||
|
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', timestamp has changed, size has changed
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||||
|
log: [error ][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== the same, with -XX:AOTMode=on
|
||||||
|
result: did not start (exit code 1)
|
||||||
|
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', timestamp has changed, size has changed
|
||||||
|
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] unrecoverable error
|
||||||
|
|
||||||
|
== a dependency jar has a different modification time (same bytes)
|
||||||
|
result: started, ready after 3865 ms (Started AotCacheApplication in 3.201 seconds)
|
||||||
|
log: [warning][aot] This file is not the one used while building the AOT cache: 'lib/spring-core-7.0.9.jar', timestamp has changed
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||||
|
log: [error ][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== the same files copied with plain cp (bytes and path layout identical, timestamps new)
|
||||||
|
result: started, ready after 4013 ms (Started AotCacheApplication in 3.361 seconds)
|
||||||
|
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', timestamp has changed
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||||
|
log: [error ][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== the application directory was moved after training (same bytes, different path)
|
||||||
|
result: started, ready after 2005 ms (Started AotCacheApplication in 1.542 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== cache trained on the fat jar, run from the extracted layout
|
||||||
|
result: started, ready after 3900 ms (Started AotCacheApplication in 3.262 seconds)
|
||||||
|
log: [warning][aot] This file is not the one used while building the AOT cache: 'app.jar', size has changed
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
|
||||||
|
log: [error ][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== JDK 25's cache used by JDK 27
|
||||||
|
result: started, ready after 4221 ms (Started AotCacheApplication in 3.537 seconds)
|
||||||
|
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||||
|
log: [warning][aot] The AOT cache has the wrong version.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||||
|
|
||||||
|
== the same, with -XX:AOTMode=on
|
||||||
|
result: did not start (exit code 1)
|
||||||
|
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||||
|
log: [warning][aot] The AOT cache has the wrong version.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
log: [warning][aot] The AOT cache version 0x13 does not match the required version 0x14.
|
||||||
|
|
||||||
|
== -XX:+UseCompactObjectHeaders added at run time only (JDK 25)
|
||||||
|
result: started, ready after 4138 ms (Started AotCacheApplication in 3.446 seconds)
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
log: [error ][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== a different collector at run time: -XX:+UseSerialGC
|
||||||
|
result: started, ready after 1916 ms (Started AotCacheApplication in 1.516 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== a different collector at run time: -XX:+UseParallelGC
|
||||||
|
result: started, ready after 1957 ms (Started AotCacheApplication in 1.473 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== a different collector at run time: -XX:+UseZGC
|
||||||
|
result: started, ready after 3981 ms (Started AotCacheApplication in 3.347 seconds)
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
[ ] The saved state of UseCompressedOops and UseCompressedClassPointers is different from runtime, CDS will be disabled.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
log: [error ][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== a different heap size at run time: -Xmx256m
|
||||||
|
result: started, ready after 2058 ms (Started AotCacheApplication in 1.602 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== a cache file that does not exist
|
||||||
|
result: started, ready after 4113 ms (Started AotCacheApplication in 3.451 seconds)
|
||||||
|
log: [error][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error][aot] Loading static archive failed.
|
||||||
|
log: [error][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== a cache file that does not exist, with -XX:AOTMode=on
|
||||||
|
result: did not start (exit code 1)
|
||||||
|
log: [error][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error][aot] Loading static archive failed.
|
||||||
|
log: [error][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
# and on JDK 27, whose cache was trained under the default collector (G1):
|
||||||
|
|
||||||
|
== JDK 27, cache from JDK 27 (control)
|
||||||
|
result: started, ready after 1955 ms (Started AotCacheApplication in 1.547 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== JDK 27, -XX:+UseZGC at run time
|
||||||
|
result: started, ready after 4158 ms (Started AotCacheApplication in 3.502 seconds)
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
[ ] The saved state of UseCompressedOops (1) is different from runtime (0), CDS will be disabled.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
log: [error ][aot] Unable to map shared spaces
|
||||||
|
|
||||||
|
== JDK 27, -XX:+UseSerialGC at run time
|
||||||
|
result: started, ready after 2103 ms (Started AotCacheApplication in 1.672 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== JDK 27, -XX:-UseCompactObjectHeaders at run time
|
||||||
|
result: started, ready after 3906 ms (Started AotCacheApplication in 3.163 seconds)
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
|
||||||
|
# why ZGC is different: ask the JVM (-Xlog:aot=info) why it refused the G1-trained cache
|
||||||
|
|
||||||
|
== JDK 27, -XX:+UseZGC with the G1-trained cache, -Xlog:aot=info
|
||||||
|
result: started, ready after 3885 ms (Started AotCacheApplication in 3.258 seconds)
|
||||||
|
log: [info][aot] The AOT cache was created with UseCompressedOops = 1, UseCompactObjectHeaders = 1
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
[ ] The saved state of UseCompressedOops (1) is different from runtime (0), CDS will be disabled.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
|
||||||
|
== JDK 27, cache trained under ZGC, run under ZGC
|
||||||
|
result: started, ready after 1988 ms (Started AotCacheApplication in 1.554 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
|
== JDK 27, cache trained under ZGC, run under G1 (compressed oops on again)
|
||||||
|
result: started, ready after 3869 ms (Started AotCacheApplication in 3.199 seconds)
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
[ ] The saved state of UseCompressedOops (0) is different from runtime (1), CDS will be disabled.
|
||||||
|
log: [error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
|
||||||
|
log: [error ][aot] Loading static archive failed.
|
||||||
|
log: [warning][aot] Unable to use AOT cache.
|
||||||
|
|
||||||
|
== JDK 27, cache trained under ZGC, run under G1 with -XX:-UseCompressedOops
|
||||||
|
result: started, ready after 2018 ms (Started AotCacheApplication in 1.534 seconds)
|
||||||
|
log: (nothing at warning or error level)
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# java -Djarmode=tools -jar app.jar extract --layers (the layer order Spring Boot's Dockerfile support uses)
|
||||||
|
layer KB gzip KB files
|
||||||
|
dependencies 22878 20490 47
|
||||||
|
spring-boot-loader 0 0 0
|
||||||
|
snapshot-dependencies 0 0 0
|
||||||
|
application 8 7 1
|
||||||
|
|
||||||
|
# the AOT cache from the training run (context-only), for scale
|
||||||
|
refresh.aot 61016 15362
|
||||||
|
|
||||||
|
# change one line of application code (the unit price in OrderService, built by prepare.sh), extract again
|
||||||
|
layer before after verdict
|
||||||
|
dependencies 81a2cb4f9f5c 81a2cb4f9f5c identical: reused from cache/registry
|
||||||
|
spring-boot-loader abcfa6a9d4df abcfa6a9d4df identical: reused from cache/registry
|
||||||
|
snapshot-dependencies abcfa6a9d4df abcfa6a9d4df identical: reused from cache/registry
|
||||||
|
application 4ecdea8230c6 19563974ba7b CHANGED: rebuilt and pushed
|
||||||
|
AOT cache: trained against the previous jar, so it is stale for the new one (see output/04-mismatch.txt); it must be regenerated after the application layer
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
$ curl localhost:18110/ping
|
||||||
|
ok [HTTP 200]
|
||||||
|
$ curl -H 'Content-Type: application/json' -d '{"customer":"[email protected]","sku":"ABC-1234","quantity":3}' localhost:18110/orders
|
||||||
|
{"id":1,"customer":"[email protected]","sku":"ABC-1234","quantity":3,"total":14.97,"createdAt":"2026-09-24T16:35:45.259486118Z"} [HTTP 201]
|
||||||
|
$ curl localhost:18110/orders/1
|
||||||
|
{"id":1,"customer":"[email protected]","sku":"ABC-1234","quantity":3,"total":14.97,"createdAt":"2026-09-24T16:35:45.259486118Z"} [HTTP 200]
|
||||||
|
$ curl -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' localhost:18110/orders
|
||||||
|
{"timestamp":"2026-09-24T16:35:45.351Z","status":400,"error":"Bad Request","path":"/orders"} [HTTP 400]
|
||||||
|
$ curl localhost:18110/orders/99
|
||||||
|
[HTTP 404]
|
||||||
|
$ curl localhost:18110/actuator/health
|
||||||
|
{"groups":["liveness","readiness"],"status":"UP"} [HTTP 200]
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?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>aot-cache</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>aot-cache</name>
|
||||||
|
<description>The JDK 25 AOT cache (JEP 514/515) on a Spring Boot 4.1 service, measured against a plain JVM, AppCDS and a GraalVM native image</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-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</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>
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates everything under output/. Takes about 30 minutes; the native build alone is about 5.
|
||||||
|
# Needs JDK 25, JDK 27, a GraalVM for JDK 25 and Maven (paths: scripts/env.sh). Use the machine for nothing else while it runs.
|
||||||
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
source scripts/env.sh
|
||||||
|
ROUNDS=${ROUNDS:-10}
|
||||||
|
mkdir -p output
|
||||||
|
bash scripts/prepare.sh > output/00-prepare.txt 2>&1
|
||||||
|
bash scripts/environment.sh > output/00-environment.txt 2>&1
|
||||||
|
bash scripts/native.sh > output/03-native-build.txt 2>&1
|
||||||
|
bash scripts/train.sh > output/01-training.txt 2>&1
|
||||||
|
bash scripts/startup.sh "$ROUNDS" > output/02-startup.txt 2> "$WORK/startup.err"
|
||||||
|
bash scripts/mismatch.sh > output/04-mismatch.txt 2>&1
|
||||||
|
bash scripts/layers.sh > output/05-layers.txt 2>&1
|
||||||
|
bash scripts/sample.sh > output/06-sample-endpoints.txt 2>&1
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Where the JDKs and GraalVM live. Override with environment variables; nothing else is hard-coded.
|
||||||
|
: "${JDK25:=/tmp/tools/j25/jdk-25.0.4.1+1}"
|
||||||
|
: "${JDK27:=/tmp/tools/j27/jdk-27+35}"
|
||||||
|
: "${GRAALVM:=$(ls -d /tmp/tools/graal/graalvm-community* 2>/dev/null | head -1)}"
|
||||||
|
export JDK25 JDK27 GRAALVM
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
WORK="$ROOT/work" # generated jars, caches and logs (git-ignored)
|
||||||
|
OUT="$ROOT/output" # transcripts that the article quotes (committed)
|
||||||
|
export ROOT WORK OUT
|
||||||
|
# Maven may need proxy/trust-store options that must not leak into the JVM runs being measured.
|
||||||
|
MVN_JTO="${JAVA_TOOL_OPTIONS:-}"
|
||||||
|
unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS
|
||||||
|
mvn_run() { JAVA_TOOL_OPTIONS="$MVN_JTO" "$@"; }
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source "$(dirname "$0")/env.sh"
|
||||||
|
echo "# machine"; echo "cpus: $(nproc) memory: $(free -m | awk '/Mem:/ {print $2}') MB kernel: $(uname -r)"
|
||||||
|
echo; echo "# JDK 25 (the JVM the article is about)"; "$JDK25/bin/java" -version 2>&1
|
||||||
|
echo; echo "# JDK 27 (the comparison)"; "$JDK27/bin/java" -version 2>&1
|
||||||
|
echo; echo "# GraalVM (native image)"; "$GRAALVM/bin/native-image" --version 2>&1 | head -2
|
||||||
|
echo; echo "# libraries"; ls "$WORK/ext/lib" | grep -E '^(spring-boot|spring-core|spring-webmvc|tomcat-embed-core|jackson-databind|hibernate-validator)-[0-9]' | sed 's/\.jar$//'
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Docker layer arithmetic without a Docker daemon: which layers of the extracted application change when one line of
|
||||||
|
# code changes, how big each layer is (uncompressed, and gzipped as a registry would store it), and where the AOT
|
||||||
|
# cache falls. A layer is "a directory tree"; two builds share a layer when the tree is byte-identical.
|
||||||
|
source "$(dirname "$0")/env.sh"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
layer_sum() { # dir -> one hash over every file's path and contents
|
||||||
|
( cd "$1" && find . -type f | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -c1-12 )
|
||||||
|
}
|
||||||
|
gz_kb() { tar -C "$1" -cf - . 2>/dev/null | gzip -6 | wc -c | awk '{printf "%d", $1/1024}'; }
|
||||||
|
kb() { find "$1" -type f -printf '%s\n' | awk '{s+=$1} END{printf "%d", s/1024}'; }
|
||||||
|
|
||||||
|
echo "# java -Djarmode=tools -jar app.jar extract --layers (the layer order Spring Boot's Dockerfile support uses)"
|
||||||
|
rm -rf "$WORK/layers-a" "$WORK/layers-b"
|
||||||
|
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app.jar" extract --layers --destination "$WORK/layers-a" >/dev/null
|
||||||
|
printf '%-22s %10s %10s %s\n' layer "KB" "gzip KB" "files"
|
||||||
|
for d in dependencies spring-boot-loader snapshot-dependencies application; do
|
||||||
|
printf '%-22s %10s %10s %s\n' "$d" "$(kb "$WORK/layers-a/$d")" "$(gz_kb "$WORK/layers-a/$d")" "$(find "$WORK/layers-a/$d" -type f | wc -l)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "# the AOT cache from the training run (context-only), for scale"
|
||||||
|
printf '%-22s %10s %10s\n' "refresh.aot" "$(( $(stat -c %s "$WORK/refresh.aot") / 1024 ))" "$(gzip -6 -c "$WORK/refresh.aot" | wc -c | awk '{printf "%d", $1/1024}')"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "# change one line of application code (the unit price in OrderService, built by prepare.sh), extract again"
|
||||||
|
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-changed.jar" extract --layers --destination "$WORK/layers-b" >/dev/null
|
||||||
|
printf '%-22s %-14s %-14s %s\n' layer "before" "after" "verdict"
|
||||||
|
for d in dependencies spring-boot-loader snapshot-dependencies application; do
|
||||||
|
a=$(layer_sum "$WORK/layers-a/$d"); b=$(layer_sum "$WORK/layers-b/$d")
|
||||||
|
printf '%-22s %-14s %-14s %s\n' "$d" "$a" "$b" "$([ "$a" = "$b" ] && echo "identical: reused from cache/registry" || echo "CHANGED: rebuilt and pushed")"
|
||||||
|
done
|
||||||
|
echo "AOT cache: trained against the previous jar, so it is stale for the new one (see output/04-mismatch.txt); it must be regenerated after the application layer"
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Start / probe / stop helpers shared by the measurement scripts.
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/env.sh"
|
||||||
|
|
||||||
|
PORT=18110
|
||||||
|
BODY='{"customer":"[email protected]","sku":"ABC-1234","quantity":3}'
|
||||||
|
|
||||||
|
now_ms() { echo $(( $(date +%s%N) / 1000000 )); }
|
||||||
|
|
||||||
|
# start_app WORKDIR LOGFILE cmd args... -> sets APP_PID (the java/native process itself, thanks to exec)
|
||||||
|
start_app() {
|
||||||
|
local wd=$1 log=$2; shift 2
|
||||||
|
( cd "$wd" && exec "$@" ) > "$log" 2>&1 &
|
||||||
|
APP_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
# wait_ready -> 0 when GET /ping answers 200 within 60 s
|
||||||
|
wait_ready() {
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 3000); do
|
||||||
|
curl -sf -o /dev/null --max-time 2 "localhost:$PORT/ping" && return 0
|
||||||
|
kill -0 "$APP_PID" 2>/dev/null || return 1
|
||||||
|
sleep 0.02
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_app() {
|
||||||
|
kill "$APP_PID" 2>/dev/null
|
||||||
|
wait "$APP_PID" 2>/dev/null
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ms_of SECONDS -> milliseconds with one decimal
|
||||||
|
ms_of() { awk -v s="$1" 'BEGIN{printf "%.1f", s*1000}'; }
|
||||||
|
|
||||||
|
# post_order -> prints curl's time_total in ms
|
||||||
|
post_order() {
|
||||||
|
ms_of "$(curl -s -o /dev/null -w '%{time_total}' -H 'Content-Type: application/json' -d "$BODY" "localhost:$PORT/orders")"
|
||||||
|
}
|
||||||
|
get_order() { ms_of "$(curl -s -o /dev/null -w '%{time_total}' "localhost:$PORT/orders/1")"; }
|
||||||
|
|
||||||
|
rss_mb() { awk '/VmRSS/ {printf "%d", $2/1024}' "/proc/$APP_PID/status"; }
|
||||||
|
|
||||||
|
median() { sort -n | awk '{a[NR]=$1} END{ if (NR==0) {print "n/a"; exit} if (NR%2) printf "%s", a[(NR+1)/2]; else printf "%.1f", (a[NR/2]+a[NR/2+1])/2 }'; }
|
||||||
|
minmax() { sort -n | awk 'NR==1{lo=$1} {hi=$1} END{printf "%s..%s", lo, hi}'; }
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# What happens when the cache does not match the run. Each case starts the service with a cache that was made
|
||||||
|
# under different conditions, and prints (1) what the JVM logged about the cache, (2) how long start-up took.
|
||||||
|
# -XX:AOTMode=auto is the default: use the cache if it is valid, otherwise carry on without it.
|
||||||
|
# -XX:AOTMode=on : refuse to start if the cache cannot be used.
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
J25=$JDK25/bin/java; J27=$JDK27/bin/java
|
||||||
|
|
||||||
|
run_case() { # title, workdir, cmd... (runs once with -Xlog:aot=warning so the cache's complaints show up)
|
||||||
|
local title=$1 wd=$2; shift 2
|
||||||
|
local t0 log="$WORK/mismatch.log"
|
||||||
|
echo "== $title"
|
||||||
|
t0=$(now_ms)
|
||||||
|
start_app "$wd" "$log" "$@" --server.port=$PORT
|
||||||
|
if wait_ready; then
|
||||||
|
echo " result: started, ready after $(( $(now_ms) - t0 )) ms ($(grep -o 'Started [A-Za-z]* in [0-9.]* seconds' "$log"))"
|
||||||
|
stop_app
|
||||||
|
else
|
||||||
|
wait "$APP_PID" 2>/dev/null
|
||||||
|
echo " result: did not start (exit code $?)"
|
||||||
|
fi
|
||||||
|
grep -E '\[(warning|error)[ ]*\]\[(aot|cds)|saved state of|created with' "$log" | grep -v 'Skipping' | head -5 | sed -E 's/^\[[0-9.]+s\]/ log: /' | cut -c1-170
|
||||||
|
[ -z "$(grep -E '\[(warning|error)[ ]*\]\[(aot|cds)' "$log" | grep -v Skipping)" ] && echo " log: (nothing at warning or error level)"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "# cache: $WORK/refresh.aot, trained on JDK 25.0.4.1, extracted layout, default GC (G1), 2 CPUs"
|
||||||
|
echo
|
||||||
|
run_case "control: the same JVM, the same jar, the same path" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
|
||||||
|
# 1. the jar changed after training
|
||||||
|
# (cp -a everywhere below: plain cp gives every file a new modification time, which the cache checks - see the timestamp case)
|
||||||
|
rm -rf "$WORK/ext-changed"; cp -a "$WORK/ext" "$WORK/ext-changed"
|
||||||
|
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-changed.jar" extract --destination "$WORK/changed-tmp" >/dev/null
|
||||||
|
cp "$WORK/changed-tmp/app-changed.jar" "$WORK/ext-changed/app.jar"; rm -rf "$WORK/changed-tmp" # only the application jar differs
|
||||||
|
run_case "the application jar was rebuilt after a one-line code change, default AOTMode=auto" "$WORK/ext-changed" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
run_case "the same, with -XX:AOTMode=on" "$WORK/ext-changed" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
|
||||||
|
# 2. a dependency jar changed
|
||||||
|
rm -rf "$WORK/ext-dep"; cp -a "$WORK/ext" "$WORK/ext-dep"
|
||||||
|
( cd "$WORK/ext-dep/lib" && f=$(ls spring-core-*.jar) && touch -d '2001-01-01' "$f" )
|
||||||
|
run_case "a dependency jar has a different modification time (same bytes)" "$WORK/ext-dep" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
|
||||||
|
# 2b. only a copy, with fresh timestamps (what `cp -r` or a re-extract does)
|
||||||
|
rm -rf "$WORK/ext-copy"; cp -r "$WORK/ext" "$WORK/ext-copy"
|
||||||
|
run_case "the same files copied with plain cp (bytes and path layout identical, timestamps new)" "$WORK/ext-copy" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
|
||||||
|
# 3. moved to another directory
|
||||||
|
rm -rf "$WORK/moved"; mkdir -p "$WORK/moved"; cp -a "$WORK/ext" "$WORK/moved/app-dir"
|
||||||
|
run_case "the application directory was moved after training (same bytes, different path)" "$WORK/moved/app-dir" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
|
||||||
|
# 4. fat jar cache used with the extracted layout
|
||||||
|
run_case "cache trained on the fat jar, run from the extracted layout" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/fat.aot" -jar app.jar
|
||||||
|
|
||||||
|
# 5. different JVM
|
||||||
|
run_case "JDK 25's cache used by JDK 27" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
run_case "the same, with -XX:AOTMode=on" "$WORK/ext" "$J27" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
|
||||||
|
# 6. flags that change what the cache holds
|
||||||
|
run_case "-XX:+UseCompactObjectHeaders added at run time only (JDK 25)" "$WORK/ext" "$J25" -XX:+UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
run_case "a different collector at run time: -XX:+UseSerialGC" "$WORK/ext" "$J25" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
run_case "a different collector at run time: -XX:+UseParallelGC" "$WORK/ext" "$J25" -XX:+UseParallelGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
run_case "a different collector at run time: -XX:+UseZGC" "$WORK/ext" "$J25" -XX:+UseZGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
run_case "a different heap size at run time: -Xmx256m" "$WORK/ext" "$J25" -Xmx256m -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||||
|
run_case "a cache file that does not exist" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
|
||||||
|
run_case "a cache file that does not exist, with -XX:AOTMode=on" "$WORK/ext" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
|
||||||
|
|
||||||
|
echo "# and on JDK 27, whose cache was trained under the default collector (G1):"
|
||||||
|
echo
|
||||||
|
run_case "JDK 27, cache from JDK 27 (control)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||||
|
run_case "JDK 27, -XX:+UseZGC at run time" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||||
|
run_case "JDK 27, -XX:+UseSerialGC at run time" "$WORK/ext" "$J27" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||||
|
run_case "JDK 27, -XX:-UseCompactObjectHeaders at run time" "$WORK/ext" "$J27" -XX:-UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||||
|
|
||||||
|
echo "# why ZGC is different: ask the JVM (-Xlog:aot=info) why it refused the G1-trained cache"
|
||||||
|
echo
|
||||||
|
run_case "JDK 27, -XX:+UseZGC with the G1-trained cache, -Xlog:aot=info" "$WORK/ext" "$J27" -XX:+UseZGC -Xlog:aot=info -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||||
|
run_case "JDK 27, cache trained under ZGC, run under ZGC" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||||
|
run_case "JDK 27, cache trained under ZGC, run under G1 (compressed oops on again)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||||
|
run_case "JDK 27, cache trained under ZGC, run under G1 with -XX:-UseCompressedOops" "$WORK/ext" "$J27" -XX:-UseCompressedOops -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds the GraalVM native image of the same service (no reflection tricks: the app has none). Takes minutes.
|
||||||
|
source "$(dirname "$0")/env.sh"
|
||||||
|
cd "$ROOT"; mkdir -p "$WORK"
|
||||||
|
t0=$(date +%s)
|
||||||
|
JAVA_HOME=$GRAALVM mvn_run mvn -B -DskipTests -Pnative native:compile > "$WORK/native-build.log" 2>&1
|
||||||
|
rc=$?
|
||||||
|
echo "# GraalVM: $("$GRAALVM/bin/native-image" --version 2>&1 | head -1)"
|
||||||
|
grep -E "types, .* fields, and .* methods found reachable|Peak RSS|Finished generating|BUILD (SUCCESS|FAILURE)|Total time" "$WORK/native-build.log" | sed -E 's/^\[INFO\] //; s/^ +//'
|
||||||
|
[ $rc -eq 0 ] && cp "target/aot-cache" "$WORK/aot-cache-native" && ls -l "$WORK/aot-cache-native" "$WORK/app.jar" | awk '{printf "%-30s %d MB\n", $NF, $5/1048576}'
|
||||||
|
echo "build wall time: $(( $(date +%s) - t0 )) s"
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds everything the measurements need under work/: the plain jar, a jar with Spring's own AOT output,
|
||||||
|
# the extracted layout, an AppCDS archive and the AOT caches. Prints what it built and how big each artifact is.
|
||||||
|
source "$(dirname "$0")/env.sh"
|
||||||
|
set -e
|
||||||
|
cd "$ROOT"
|
||||||
|
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||||
|
|
||||||
|
echo "== build: plain jar (mvn package)"
|
||||||
|
rm -rf "$ROOT/target" # a stale target/ would carry Spring AOT output from an earlier -Pnative build into the plain jar
|
||||||
|
JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests package > "$WORK/build-plain.log" 2>&1
|
||||||
|
cp target/app.jar "$WORK/app.jar"
|
||||||
|
rm -rf "$ROOT/target"
|
||||||
|
echo "== build: jar with Spring AOT output (mvn -Pnative package; this does NOT compile a native image)"
|
||||||
|
JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests -Pnative package > "$WORK/build-springaot.log" 2>&1
|
||||||
|
cp target/app.jar "$WORK/app-springaot.jar"
|
||||||
|
echo "initializers in plain jar: $(unzip -l "$WORK/app.jar" | grep -c '__ApplicationContextInitializer')"
|
||||||
|
echo "initializers in Spring AOT jar: $(unzip -l "$WORK/app-springaot.jar" | grep -c '__ApplicationContextInitializer')"
|
||||||
|
|
||||||
|
echo "== extract both jars (java -Djarmode=tools -jar app.jar extract)"
|
||||||
|
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app.jar" extract --destination "$WORK/ext" >/dev/null
|
||||||
|
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-springaot.jar" extract --destination "$WORK/ext-springaot" >/dev/null
|
||||||
|
mv "$WORK/ext-springaot/app-springaot.jar" "$WORK/ext-springaot/app.jar" # the launcher jar keeps its lib/ Class-Path
|
||||||
|
ls "$WORK/ext" "$WORK/ext-springaot"
|
||||||
|
|
||||||
|
echo "== build: the same service after a one-line change (the unit price in OrderService), for the mismatch and layer checks"
|
||||||
|
mkdir -p "$WORK/changed-src"
|
||||||
|
cp -r pom.xml src "$WORK/changed-src/"
|
||||||
|
sed -i 's/BigDecimal.valueOf(4_99, 2)/BigDecimal.valueOf(5_99, 2)/' "$WORK/changed-src/src/main/java/com/ankurm/aotcache/OrderService.java"
|
||||||
|
grep -n 'valueOf(5_99' "$WORK/changed-src/src/main/java/com/ankurm/aotcache/OrderService.java" | sed 's/^/edited: /'
|
||||||
|
( cd "$WORK/changed-src" && JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests package > "$WORK/build-changed.log" 2>&1 )
|
||||||
|
cp "$WORK/changed-src/target/app.jar" "$WORK/app-changed.jar"
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# A transcript of the service's own endpoints, so the reader knows what the training run's traffic is.
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
start_app "$WORK/ext" "$WORK/sample.log" "$JDK25/bin/java" -jar app.jar --server.port=$PORT
|
||||||
|
wait_ready || { echo "did not start"; exit 1; }
|
||||||
|
show() { # print the command the way a shell would need it typed, then run it
|
||||||
|
printf '$'; for a in "$@"; do case $a in *[' {}"']*) printf " '%s'" "$a";; *) printf ' %s' "$a";; esac; done; echo
|
||||||
|
"$@" -s -w ' [HTTP %{http_code}]\n'
|
||||||
|
}
|
||||||
|
show curl "localhost:$PORT/ping"
|
||||||
|
show curl -H 'Content-Type: application/json' -d "$BODY" "localhost:$PORT/orders"
|
||||||
|
show curl "localhost:$PORT/orders/1"
|
||||||
|
show curl -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' "localhost:$PORT/orders"
|
||||||
|
show curl "localhost:$PORT/orders/99"
|
||||||
|
show curl "localhost:$PORT/actuator/health"
|
||||||
|
stop_app
|
||||||
|
exit 0
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Startup and first-request latency for every configuration. Configurations are run in interleaved rounds
|
||||||
|
# (round 1 runs all of them once, then round 2, ...) so slow drift on a shared machine hits all of them alike.
|
||||||
|
# scripts/startup.sh [ROUNDS] default 10
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
ROUNDS=${1:-10}
|
||||||
|
LOG="$WORK/startup-runs.txt"; : > "$LOG"
|
||||||
|
|
||||||
|
declare -A WD CMD
|
||||||
|
labels=()
|
||||||
|
add_cfg() { local l=$1 wd=$2; shift 2; labels+=("$l"); WD[$l]=$wd; CMD[$l]="$*"; }
|
||||||
|
|
||||||
|
J25=$JDK25/bin/java; J27=$JDK27/bin/java
|
||||||
|
add_cfg "1 jvm-fat-jar" "$WORK" "$J25 -jar app.jar"
|
||||||
|
add_cfg "2 jvm-extracted" "$WORK/ext" "$J25 -jar app.jar"
|
||||||
|
add_cfg "3 jvm+spring-aot" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -jar app.jar"
|
||||||
|
add_cfg "4 appcds" "$WORK/ext" "$J25 -XX:SharedArchiveFile=$WORK/appcds.jsa -jar app.jar"
|
||||||
|
add_cfg "5 aot-cache fat-jar" "$WORK" "$J25 -XX:AOTCache=$WORK/fat.aot -jar app.jar"
|
||||||
|
add_cfg "6 aot-cache context-only" "$WORK/ext" "$J25 -XX:AOTCache=$WORK/refresh.aot -jar app.jar"
|
||||||
|
add_cfg "7 aot-cache traffic" "$WORK/ext" "$J25 -XX:AOTCache=$WORK/traffic.aot -jar app.jar"
|
||||||
|
add_cfg "8 aot-cache+spring-aot" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -XX:AOTCache=$WORK/springaot.aot -jar app.jar"
|
||||||
|
add_cfg "9 aot-cache+spring-aot traffic" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -XX:AOTCache=$WORK/springaot-traffic.aot -jar app.jar"
|
||||||
|
add_cfg "10 jdk27 plain" "$WORK/ext" "$J27 -jar app.jar"
|
||||||
|
add_cfg "11 jdk27 aot-cache" "$WORK/ext" "$J27 -XX:AOTCache=$WORK/refresh27.aot -jar app.jar"
|
||||||
|
[ -x "$WORK/aot-cache-native" ] && add_cfg "12 native image" "$WORK" "$WORK/aot-cache-native"
|
||||||
|
|
||||||
|
one_run() { # label round
|
||||||
|
local l=$1 r=$2 t0 ready started first second med50 rss
|
||||||
|
t0=$(now_ms)
|
||||||
|
start_app "${WD[$l]}" "$WORK/run.log" ${CMD[$l]} --server.port=$PORT
|
||||||
|
if ! wait_ready; then echo "RUN|$l|$r|FAILED" >> "$LOG"; stop_app; return; fi
|
||||||
|
ready=$(( $(now_ms) - t0 ))
|
||||||
|
started=$(grep -o 'Started [A-Za-z]* in [0-9.]* seconds' "$WORK/run.log" | awk '{print $4*1000}')
|
||||||
|
first=$(post_order); second=$(get_order)
|
||||||
|
med50=$(for i in $(seq 1 50); do post_order; echo; done | grep . | median)
|
||||||
|
rss=$(rss_mb)
|
||||||
|
echo "RUN|$l|$r|ready=$ready|started=${started:-n/a}|first=$first|second=$second|next50=$med50|rss=$rss" >> "$LOG"
|
||||||
|
stop_app; sleep 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "# ${#labels[@]} configurations x $ROUNDS rounds; one unrecorded warm-up round first (so cache files are in the page cache)"
|
||||||
|
for l in "${labels[@]}"; do one_run "$l" 0; done; : > "$LOG"
|
||||||
|
for r in $(seq 1 $ROUNDS); do
|
||||||
|
for l in "${labels[@]}"; do one_run "$l" "$r"; done
|
||||||
|
echo "round $r done" >&2
|
||||||
|
done
|
||||||
|
|
||||||
|
field() { grep -F "RUN|$1|" "$LOG" | grep -o "$2=[0-9.]*" | cut -d= -f2; }
|
||||||
|
echo
|
||||||
|
echo "== medians of $ROUNDS runs (min..max in brackets), milliseconds; rss in MB"
|
||||||
|
printf '%-32s %-20s %-20s %-20s %-18s %s\n' "configuration" "ready (exec->200)" "Boot 'Started in'" "1st POST /orders" "next 50 (median)" "RSS"
|
||||||
|
for l in "${labels[@]}"; do
|
||||||
|
f=$(grep -cF "RUN|$l|" "$LOG"); ok=$(grep -F "RUN|$l|" "$LOG" | grep -vc FAILED)
|
||||||
|
cell() { local v; v=$(field "$l" "$1"); printf '%-20s ' "$(echo "$v" | median) [$(echo "$v" | minmax)]"; }
|
||||||
|
printf '%-32s ' "$l"; cell ready; cell started; cell first; printf '%-18s ' "$(field "$l" next50 | median)"; printf '%s' "$(field "$l" rss | median)"
|
||||||
|
[ "$ok" != "$f" ] && printf ' (%s of %s runs failed)' $((f-ok)) $f
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "== every run"
|
||||||
|
sed 's/^RUN|//; s/|/ /g' "$LOG"
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# The training runs: one per cache flavour. Prints how long each took, how big the artifact is, and what the
|
||||||
|
# JVM logged at warning level while writing it. Needs scripts/prepare.sh to have run.
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
|
||||||
|
exercise() { # the traffic a "real" training run sends: success, validation failure, 404, actuator
|
||||||
|
for i in 1 2 3 4 5; do post_order >/dev/null; done
|
||||||
|
get_order >/dev/null
|
||||||
|
curl -s -o /dev/null -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' "localhost:$PORT/orders"
|
||||||
|
curl -s -o /dev/null "localhost:$PORT/orders/99"
|
||||||
|
curl -s -o /dev/null "localhost:$PORT/actuator/health"
|
||||||
|
}
|
||||||
|
|
||||||
|
report() { # label file t0
|
||||||
|
printf '%-34s %6d ms %s (%s MB)\n' "$1" $(( $(now_ms) - $3 )) "$(basename "$2")" "$(( $(stat -c %s "$2") / 1048576 ))"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "# training runs (JDK 25.0.4.1 unless noted); wall time includes JVM start, context start and cache creation"
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
( cd "$WORK/ext" && "$JDK25/bin/java" -XX:ArchiveClassesAtExit="$WORK/appcds.jsa" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-appcds.log" 2>&1
|
||||||
|
report "AppCDS (ArchiveClassesAtExit)" "$WORK/appcds.jsa" $t0
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
"$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/fat.aot" -Dspring.context.exit=onRefresh -jar "$WORK/app.jar" > "$WORK/train-fat.log" 2>&1
|
||||||
|
report "AOT cache, fat jar, context-only" "$WORK/fat.aot" $t0
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
( cd "$WORK/ext" && "$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/refresh.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-refresh.log" 2>&1
|
||||||
|
report "AOT cache, context-only training" "$WORK/refresh.aot" $t0
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
start_app "$WORK/ext" "$WORK/train-traffic.log" "$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/traffic.aot" -jar app.jar --server.port=$PORT
|
||||||
|
wait_ready && exercise
|
||||||
|
kill -TERM "$APP_PID"; wait "$APP_PID" 2>/dev/null
|
||||||
|
report "AOT cache, traffic training" "$WORK/traffic.aot" $t0
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
( cd "$WORK/ext-springaot" && "$JDK25/bin/java" -Dspring.aot.enabled=true -XX:AOTCacheOutput="$WORK/springaot.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-springaot.log" 2>&1
|
||||||
|
report "AOT cache + Spring AOT, context-only" "$WORK/springaot.aot" $t0
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
start_app "$WORK/ext-springaot" "$WORK/train-springaot-traffic.log" "$JDK25/bin/java" -Dspring.aot.enabled=true -XX:AOTCacheOutput="$WORK/springaot-traffic.aot" -jar app.jar --server.port=$PORT
|
||||||
|
wait_ready && exercise
|
||||||
|
kill -TERM "$APP_PID"; wait "$APP_PID" 2>/dev/null
|
||||||
|
report "AOT cache + Spring AOT, traffic" "$WORK/springaot-traffic.aot" $t0
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
( cd "$WORK/ext" && "$JDK27/bin/java" -XX:AOTCacheOutput="$WORK/refresh27.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-refresh27.log" 2>&1
|
||||||
|
report "AOT cache on JDK 27, context-only" "$WORK/refresh27.aot" $t0
|
||||||
|
|
||||||
|
t0=$(now_ms)
|
||||||
|
( cd "$WORK/ext" && "$JDK27/bin/java" -XX:+UseZGC -XX:AOTCacheOutput="$WORK/zgc27.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-zgc27.log" 2>&1
|
||||||
|
report "AOT cache on JDK 27, trained under ZGC" "$WORK/zgc27.aot" $t0
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "# what the JVM said at warning level while writing the context-only AOT cache (count, then the distinct kinds)"
|
||||||
|
grep -c '\[warning\]\[aot\]' "$WORK/train-refresh.log" | sed 's/^/warning lines: /'
|
||||||
|
grep '\[warning\]\[aot\]' "$WORK/train-refresh.log" | sed -E 's/^\[[0-9.]+s\]//; s/(Skipping) [^ ]+:/\1 <class>:/' | sort | uniq -c | sort -rn | head -5
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ankurm.aotcache;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class AotCacheApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(AotCacheApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ankurm.aotcache;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
/** The request body of POST /orders. Bean Validation and Jackson both have to work on it. */
|
||||||
|
public record NewOrder(
|
||||||
|
@NotBlank @Email String customer,
|
||||||
|
@NotBlank @Pattern(regexp = "[A-Z]{3}-[0-9]{4}") String sku,
|
||||||
|
@Min(1) @Max(1000) int quantity) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ankurm.aotcache;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/** A stored order. */
|
||||||
|
public record Order(long id, String customer, String sku, int quantity, BigDecimal total, Instant createdAt) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ankurm.aotcache;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class OrderController {
|
||||||
|
|
||||||
|
private final OrderService service;
|
||||||
|
|
||||||
|
public OrderController(OrderService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ping")
|
||||||
|
public String ping() {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/orders")
|
||||||
|
public ResponseEntity<Order> create(@Valid @RequestBody NewOrder body) {
|
||||||
|
Order o = service.create(body);
|
||||||
|
return ResponseEntity.created(URI.create("/orders/" + o.id())).body(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/orders/{id}")
|
||||||
|
public ResponseEntity<Order> get(@PathVariable long id) {
|
||||||
|
return service.find(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ankurm.aotcache;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OrderService {
|
||||||
|
|
||||||
|
private final AtomicLong ids = new AtomicLong();
|
||||||
|
private final Map<Long, Order> orders = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public Order create(NewOrder in) {
|
||||||
|
long id = ids.incrementAndGet();
|
||||||
|
BigDecimal unit = BigDecimal.valueOf(4_99, 2);
|
||||||
|
Order o = new Order(id, in.customer(), in.sku(), in.quantity(), unit.multiply(BigDecimal.valueOf(in.quantity())), Instant.now());
|
||||||
|
orders.put(id, o);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Order> find(long id) {
|
||||||
|
return Optional.ofNullable(orders.get(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
spring.application.name=aot-cache
|
||||||
|
management.endpoints.web.exposure.include=health
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# batch-5-to-6: Spring Batch 5 to 6 on Spring Boot 3.5 to 4.1
|
||||||
|
|
||||||
|
Companion code for the ankurm.com post "Spring Batch 5 to 6 Migration Guide".
|
||||||
|
|
||||||
|
| Folder | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `legacy/` | A one-job app on Boot 3.5.16 / Batch 5.2.6 (JDK 21) using the Batch 5 API |
|
||||||
|
| `migrated/` | The same app on Boot 4.1.1 / Batch 6.0.5 (JDK 25) using the Batch 6 API |
|
||||||
|
| `scripts/run-all.sh` | Builds the intermediate stages from `legacy/` and regenerates every file in `output/` |
|
||||||
|
| `output/` | Transcripts: compile errors, startup failure, deprecation warnings, the silent listener change, and the final run |
|
||||||
|
|
||||||
|
Batch 6.0.5 is the latest GA; 6.1.0-M1 and 6.1.0-M2 are milestones and are not tested here.
|
||||||
|
Tested on 28 September 2026.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?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>3.5.16</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>batch-legacy</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<properties><java.version>21</java.version></properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-batch</artifactId></dependency>
|
||||||
|
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
|
||||||
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
|
||||||
|
<dependency><groupId>org.springframework.batch</groupId><artifactId>spring-batch-test</artifactId><scope>test</scope></dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class App {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args);
|
||||||
|
ctx.getBean(JobRunner.class).run();
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.batch.core.ChunkListener;
|
||||||
|
import org.springframework.batch.core.Job;
|
||||||
|
import org.springframework.batch.core.Step;
|
||||||
|
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||||
|
import org.springframework.batch.core.repository.JobRepository;
|
||||||
|
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||||
|
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||||
|
import org.springframework.batch.item.ItemReader;
|
||||||
|
import org.springframework.batch.item.ItemWriter;
|
||||||
|
import org.springframework.batch.item.support.ListItemReader;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class BatchConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ItemReader<Integer> reader() {
|
||||||
|
return new ListItemReader<>(List.of(1, 2, 3, 4, 5, 6, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ItemWriter<String> writer() {
|
||||||
|
return chunk -> System.out.println("WRITE " + chunk.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ChunkListener chunkListener() {
|
||||||
|
return new ChunkListener() {
|
||||||
|
@Override public void afterChunk(ChunkContext context) {
|
||||||
|
System.out.println("CHUNK DONE, read so far=" + context.getStepContext().getStepExecution().getReadCount());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Step squareStep(JobRepository repo, PlatformTransactionManager tx, ChunkListener listener) {
|
||||||
|
return new StepBuilder("squareStep", repo)
|
||||||
|
.<Integer, String>chunk(3, tx)
|
||||||
|
.reader(reader())
|
||||||
|
.processor(i -> "sq" + (i * i))
|
||||||
|
.writer(writer())
|
||||||
|
.listener(listener)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Job squareJob(JobRepository repo, Step squareStep) {
|
||||||
|
return new JobBuilder("squareJob", repo).start(squareStep).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import org.springframework.batch.core.BatchStatus;
|
||||||
|
import org.springframework.batch.core.Job;
|
||||||
|
import org.springframework.batch.core.JobExecution;
|
||||||
|
import org.springframework.batch.core.JobParameters;
|
||||||
|
import org.springframework.batch.core.JobParametersBuilder;
|
||||||
|
import org.springframework.batch.core.explore.JobExplorer;
|
||||||
|
import org.springframework.batch.core.launch.JobLauncher;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JobRunner {
|
||||||
|
private final JobLauncher launcher;
|
||||||
|
private final JobExplorer explorer;
|
||||||
|
private final Job job;
|
||||||
|
|
||||||
|
public JobRunner(JobLauncher launcher, JobExplorer explorer, Job squareJob) {
|
||||||
|
this.launcher = launcher; this.explorer = explorer; this.job = squareJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
JobParameters params = new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
|
||||||
|
JobExecution exec = launcher.run(job, params);
|
||||||
|
System.out.println("STATUS " + exec.getStatus() + " exit=" + exec.getExitStatus().getExitCode());
|
||||||
|
System.out.println("INSTANCES " + explorer.getJobInstanceCount("squareJob"));
|
||||||
|
System.out.println("DONE " + (exec.getStatus() == BatchStatus.COMPLETED));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
spring.batch.job.enabled=false
|
||||||
|
spring.datasource.url=jdbc:h2:mem:legacy;DB_CLOSE_DELAY=-1
|
||||||
|
spring.batch.jdbc.initialize-schema=always
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.batch.core.BatchStatus;
|
||||||
|
import org.springframework.batch.core.JobExecution;
|
||||||
|
import org.springframework.batch.test.JobLauncherTestUtils;
|
||||||
|
import org.springframework.batch.test.context.SpringBatchTest;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@SpringBatchTest
|
||||||
|
class JobTest {
|
||||||
|
@Autowired JobLauncherTestUtils utils;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jobCompletes() throws Exception {
|
||||||
|
JobExecution exec = utils.launchJob();
|
||||||
|
assertThat(exec.getStatus()).isEqualTo(BatchStatus.COMPLETED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?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>batch-migrated</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<properties><java.version>25</java.version></properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-batch-jdbc</artifactId></dependency>
|
||||||
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency>
|
||||||
|
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
|
||||||
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
|
||||||
|
<dependency><groupId>org.springframework.batch</groupId><artifactId>spring-batch-test</artifactId><scope>test</scope></dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class App {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args);
|
||||||
|
ctx.getBean(JobRunner.class).run();
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.batch.core.listener.ChunkListener;
|
||||||
|
import org.springframework.batch.core.job.Job;
|
||||||
|
import org.springframework.batch.core.step.Step;
|
||||||
|
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||||
|
import org.springframework.batch.core.repository.JobRepository;
|
||||||
|
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||||
|
import org.springframework.batch.infrastructure.item.Chunk;
|
||||||
|
import org.springframework.batch.infrastructure.item.ItemReader;
|
||||||
|
import org.springframework.batch.infrastructure.item.ItemWriter;
|
||||||
|
import org.springframework.batch.infrastructure.item.support.ListItemReader;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class BatchConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ItemReader<Integer> reader() {
|
||||||
|
return new ListItemReader<>(List.of(1, 2, 3, 4, 5, 6, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ItemWriter<String> writer() {
|
||||||
|
return chunk -> System.out.println("WRITE " + chunk.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ChunkListener<Integer, String> chunkListener() {
|
||||||
|
return new ChunkListener<>() {
|
||||||
|
@Override public void afterChunk(Chunk<String> chunk) {
|
||||||
|
System.out.println("CHUNK DONE, items in chunk=" + chunk.size());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Step squareStep(JobRepository repo, PlatformTransactionManager tx, ChunkListener<Integer, String> listener) {
|
||||||
|
return new StepBuilder("squareStep", repo)
|
||||||
|
.<Integer, String>chunk(3)
|
||||||
|
.transactionManager(tx)
|
||||||
|
.reader(reader())
|
||||||
|
.processor(i -> "sq" + (i * i))
|
||||||
|
.writer(writer())
|
||||||
|
.listener(listener)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Job squareJob(JobRepository repo, Step squareStep) {
|
||||||
|
return new JobBuilder("squareJob", repo).start(squareStep).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import org.springframework.batch.core.BatchStatus;
|
||||||
|
import org.springframework.batch.core.job.Job;
|
||||||
|
import org.springframework.batch.core.job.JobExecution;
|
||||||
|
import org.springframework.batch.core.job.parameters.JobParameters;
|
||||||
|
import org.springframework.batch.core.job.parameters.JobParametersBuilder;
|
||||||
|
import org.springframework.batch.core.launch.JobOperator;
|
||||||
|
import org.springframework.batch.core.repository.JobRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JobRunner {
|
||||||
|
private final JobOperator operator;
|
||||||
|
private final JobRepository repository;
|
||||||
|
private final Job job;
|
||||||
|
|
||||||
|
public JobRunner(JobOperator operator, JobRepository repository, Job squareJob) {
|
||||||
|
this.operator = operator; this.repository = repository; this.job = squareJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
JobParameters params = new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
|
||||||
|
JobExecution exec = operator.start(job, params);
|
||||||
|
System.out.println("STATUS " + exec.getStatus() + " exit=" + exec.getExitStatus().getExitCode());
|
||||||
|
System.out.println("INSTANCES " + repository.getJobInstanceCount("squareJob"));
|
||||||
|
System.out.println("DONE " + (exec.getStatus() == BatchStatus.COMPLETED));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
spring.batch.job.enabled=false
|
||||||
|
spring.datasource.url=jdbc:h2:mem:legacy;DB_CLOSE_DELAY=-1
|
||||||
|
spring.batch.jdbc.initialize-schema=always
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ankurm.batch;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.batch.core.BatchStatus;
|
||||||
|
import org.springframework.batch.core.job.JobExecution;
|
||||||
|
import org.springframework.batch.test.JobOperatorTestUtils;
|
||||||
|
import org.springframework.batch.test.context.SpringBatchTest;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@SpringBatchTest
|
||||||
|
class JobTest {
|
||||||
|
@Autowired JobOperatorTestUtils utils;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jobCompletes() throws Exception {
|
||||||
|
JobExecution exec = utils.startJob();
|
||||||
|
assertThat(exec.getStatus()).isEqualTo(BatchStatus.COMPLETED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
$ mvn test # Boot 3.5.16, Batch 5.2.6, JDK 21
|
||||||
|
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.750 s -- in com.ankurm.batch.JobTest
|
||||||
|
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
[INFO] BUILD SUCCESS
|
||||||
|
|
||||||
|
$ java -jar target/batch-legacy-1.0.0.jar
|
||||||
|
WRITE [sq1, sq4, sq9]
|
||||||
|
CHUNK DONE, read so far=3
|
||||||
|
WRITE [sq16, sq25, sq36]
|
||||||
|
CHUNK DONE, read so far=6
|
||||||
|
WRITE [sq49]
|
||||||
|
CHUNK DONE, read so far=7
|
||||||
|
STATUS COMPLETED exit=COMPLETED
|
||||||
|
INSTANCES 1
|
||||||
|
DONE true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
$ mvn compile # legacy sources, only the Boot version changed to 4.1.1 (Batch 6.0.5)
|
||||||
|
compile error lines reported: 56
|
||||||
|
|
||||||
|
packages that no longer exist:
|
||||||
|
package org.springframework.batch.core.explore does not exist
|
||||||
|
package org.springframework.batch.item does not exist
|
||||||
|
package org.springframework.batch.item.support does not exist
|
||||||
|
|
||||||
|
classes that moved:
|
||||||
|
ChunkListener ItemReader ItemWriter Job JobExecution JobExplorer JobParameters JobParametersBuilder ListItemReader Step
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
$ mvn package # imports ported to the new packages, dependencies unchanged (spring-boot-starter-batch)
|
||||||
|
[WARNING] s2/src/main/java/com/ankurm/batch/BatchConfig.java:[34,35] afterChunk(org.springframework.batch.core.scope.context.ChunkContext) in org.springframework.batch.core.listener.ChunkListener has been deprecated and marked for removal
|
||||||
|
[WARNING] s2/src/main/java/com/ankurm/batch/BatchConfig.java:[43,17] <I,O>chunk(int,org.springframework.transaction.PlatformTransactionManager) in org.springframework.batch.core.step.builder.StepBuilder has been deprecated and marked for removal
|
||||||
|
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[14,19] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
|
||||||
|
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[15,19] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
|
||||||
|
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[18,22] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
|
||||||
|
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[18,44] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
|
||||||
|
[WARNING] s2/src/test/java/com/ankurm/batch/JobTest.java:[16,16] org.springframework.batch.test.JobLauncherTestUtils in org.springframework.batch.test has been deprecated and marked for removal
|
||||||
|
[WARNING] s2/src/test/java/com/ankurm/batch/JobTest.java:[20,34] launchJob() in org.springframework.batch.test.JobLauncherTestUtils has been deprecated and marked for removal
|
||||||
|
[INFO] BUILD SUCCESS
|
||||||
|
|
||||||
|
$ java -jar target/batch-legacy-1.0.0.jar
|
||||||
|
APPLICATION FAILED TO START
|
||||||
|
***************************
|
||||||
|
|
||||||
|
Description:
|
||||||
|
|
||||||
|
Parameter 1 of method squareStep in com.ankurm.batch.BatchConfig required a bean of type 'org.springframework.transaction.PlatformTransactionManager' that could not be found.
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
$ mvn package # + spring-boot-starter-batch-jdbc and spring-boot-starter-jdbc
|
||||||
|
[WARNING] s3/src/main/java/com/ankurm/batch/BatchConfig.java:[34,35] afterChunk(org.springframework.batch.core.scope.context.ChunkContext) in org.springframework.batch.core.listener.ChunkListener has been deprecated and marked for removal
|
||||||
|
[WARNING] s3/src/main/java/com/ankurm/batch/BatchConfig.java:[43,17] <I,O>chunk(int,org.springframework.transaction.PlatformTransactionManager) in org.springframework.batch.core.step.builder.StepBuilder has been deprecated and marked for removal
|
||||||
|
[WARNING] s3/src/main/java/com/ankurm/batch/JobRunner.java:[14,19] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
|
||||||
|
[WARNING] s3/src/main/java/com/ankurm/batch/JobRunner.java:[15,19] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
|
||||||
|
[WARNING] s3/src/main/java/com/ankurm/batch/JobRunner.java:[18,22] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
|
||||||
|
[WARNING] s3/src/main/java/com/ankurm/batch/JobRunner.java:[18,44] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
|
||||||
|
[WARNING] s3/src/test/java/com/ankurm/batch/JobTest.java:[16,16] org.springframework.batch.test.JobLauncherTestUtils in org.springframework.batch.test has been deprecated and marked for removal
|
||||||
|
[WARNING] s3/src/test/java/com/ankurm/batch/JobTest.java:[20,34] launchJob() in org.springframework.batch.test.JobLauncherTestUtils has been deprecated and marked for removal
|
||||||
|
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.571 s -- in com.ankurm.batch.JobTest
|
||||||
|
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
[INFO] BUILD SUCCESS
|
||||||
|
|
||||||
|
$ java -jar target/batch-legacy-1.0.0.jar
|
||||||
|
WRITE [sq1, sq4, sq9]
|
||||||
|
CHUNK DONE, read so far=3
|
||||||
|
WRITE [sq16, sq25, sq36]
|
||||||
|
CHUNK DONE, read so far=6
|
||||||
|
WRITE [sq49]
|
||||||
|
CHUNK DONE, read so far=7
|
||||||
|
STATUS COMPLETED exit=COMPLETED
|
||||||
|
INSTANCES 1
|
||||||
|
DONE true
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
$ mvn package # chunk(3, tx) replaced by chunk(3).transactionManager(tx); listener untouched
|
||||||
|
[WARNING] s4/src/main/java/com/ankurm/batch/BatchConfig.java:[34,35] afterChunk(org.springframework.batch.core.scope.context.ChunkContext) in org.springframework.batch.core.listener.ChunkListener has been deprecated and marked for removal
|
||||||
|
[WARNING] s4/src/main/java/com/ankurm/batch/JobRunner.java:[14,19] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
|
||||||
|
[WARNING] s4/src/main/java/com/ankurm/batch/JobRunner.java:[15,19] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
|
||||||
|
[WARNING] s4/src/main/java/com/ankurm/batch/JobRunner.java:[18,22] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
|
||||||
|
[WARNING] s4/src/main/java/com/ankurm/batch/JobRunner.java:[18,44] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
|
||||||
|
[WARNING] s4/src/test/java/com/ankurm/batch/JobTest.java:[16,16] org.springframework.batch.test.JobLauncherTestUtils in org.springframework.batch.test has been deprecated and marked for removal
|
||||||
|
[WARNING] s4/src/test/java/com/ankurm/batch/JobTest.java:[20,34] launchJob() in org.springframework.batch.test.JobLauncherTestUtils has been deprecated and marked for removal
|
||||||
|
[INFO] BUILD SUCCESS
|
||||||
|
|
||||||
|
$ java -jar target/batch-legacy-1.0.0.jar
|
||||||
|
WRITE [sq1, sq4, sq9]
|
||||||
|
WRITE [sq16, sq25, sq36]
|
||||||
|
WRITE [sq49]
|
||||||
|
STATUS COMPLETED exit=COMPLETED
|
||||||
|
INSTANCES 1
|
||||||
|
DONE true
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
$ mvn package # final migrated project
|
||||||
|
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.029 s -- in com.ankurm.batch.JobTest
|
||||||
|
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
[INFO] BUILD SUCCESS
|
||||||
|
|
||||||
|
$ java -jar target/batch-migrated-1.0.0.jar
|
||||||
|
WRITE [sq1, sq4, sq9]
|
||||||
|
CHUNK DONE, items in chunk=3
|
||||||
|
WRITE [sq16, sq25, sq36]
|
||||||
|
CHUNK DONE, items in chunk=3
|
||||||
|
WRITE [sq49]
|
||||||
|
CHUNK DONE, items in chunk=1
|
||||||
|
STATUS COMPLETED exit=COMPLETED
|
||||||
|
INSTANCES 1
|
||||||
|
DONE true
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
$ javap (Batch 6.0.5 jars), selected lines
|
||||||
|
public interface core.launch.JobOperator extends core.launch.JobLauncher {
|
||||||
|
public interface core.repository.JobRepository extends core.repository.explore.JobExplorer {
|
||||||
|
public class test.JobOperatorTestUtils extends test.JobLauncherTestUtils {
|
||||||
|
public <I, O> core.step.builder.SimpleStepBuilder<I, O> chunk(int, PlatformTransactionManager);
|
||||||
|
public <I, O> core.step.builder.ChunkOrientedStepBuilder<I, O> chunk(int);
|
||||||
|
public <I, O> core.step.builder.SimpleStepBuilder<I, O> chunk(infrastructure.repeat.CompletionPolicy, PlatformTransactionManager);
|
||||||
|
public default void beforeChunk(core.scope.context.ChunkContext);
|
||||||
|
public default void afterChunk(core.scope.context.ChunkContext);
|
||||||
|
public default void afterChunkError(core.scope.context.ChunkContext);
|
||||||
|
public default void beforeChunk(infrastructure.item.Chunk<I>);
|
||||||
|
public default void afterChunk(infrastructure.item.Chunk<O>);
|
||||||
|
public default void onChunkError(java.lang.Exception, infrastructure.item.Chunk<O>);
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates output/*.txt. Needs Maven, JDK 21 (J21) and JDK 25 (J25); network for Maven Central.
|
||||||
|
set -u
|
||||||
|
HERE=$(cd "$(dirname "$0")/.." && pwd); OUT=$HERE/output; W=${WORK:-/tmp/b56-work}
|
||||||
|
J21=${J21:-/usr/lib/jvm/java-21-openjdk-amd64}; J25=${J25:-/tmp/tools/jdk-25.0.4.1+1}; MVN=${MVN:-mvn}
|
||||||
|
mkdir -p "$OUT"; rm -rf "$W"; mkdir -p "$W"
|
||||||
|
mv_() { local jh=$1 d=$2; shift 2; (cd "$d" && JAVA_HOME=$jh $MVN -B -Dmaven.compiler.showDeprecation=true "$@" 2>&1 | grep -v JAVA_TOOL_OPTIONS | sed "s#$W/##g"); }
|
||||||
|
runjar() { local jh=$1 jar=$2; JAVA_HOME=$jh $jh/bin/java -jar "$jar" 2>&1 | grep -E '^(WRITE|CHUNK|STATUS|INSTANCES|DONE)'; }
|
||||||
|
stage() { rm -rf "$W/$1"; cp -r "$HERE/$2" "$W/$1"; rm -rf "$W/$1/target"; }
|
||||||
|
PORT_IMPORTS=$(cat <<'PY'
|
||||||
|
import glob,sys
|
||||||
|
m={'org.springframework.batch.core.ChunkListener':'org.springframework.batch.core.listener.ChunkListener',
|
||||||
|
'org.springframework.batch.core.Job;':'org.springframework.batch.core.job.Job;',
|
||||||
|
'org.springframework.batch.core.Step;':'org.springframework.batch.core.step.Step;',
|
||||||
|
'org.springframework.batch.core.JobExecution;':'org.springframework.batch.core.job.JobExecution;',
|
||||||
|
'org.springframework.batch.core.JobParameters;':'org.springframework.batch.core.job.parameters.JobParameters;',
|
||||||
|
'org.springframework.batch.core.JobParametersBuilder':'org.springframework.batch.core.job.parameters.JobParametersBuilder',
|
||||||
|
'org.springframework.batch.core.explore.JobExplorer':'org.springframework.batch.core.repository.explore.JobExplorer',
|
||||||
|
'org.springframework.batch.item.':'org.springframework.batch.infrastructure.item.'}
|
||||||
|
for f in glob.glob(sys.argv[1]+'/src/**/*.java',recursive=True):
|
||||||
|
s=open(f).read()
|
||||||
|
for a,b in m.items(): s=s.replace(a,b)
|
||||||
|
open(f,'w').write(s)
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
# 01 legacy: Boot 3.5.16 / Batch 5.2.6
|
||||||
|
stage s0 legacy
|
||||||
|
{ echo '$ mvn test # Boot 3.5.16, Batch 5.2.6, JDK 21'; mv_ "$J21" "$W/s0" package | grep -E 'Tests run:|BUILD'
|
||||||
|
echo; echo '$ java -jar target/batch-legacy-1.0.0.jar'; runjar "$J21" "$W/s0/target/batch-legacy-1.0.0.jar"; } > "$OUT/01-legacy-batch5.txt"
|
||||||
|
# 02 same sources, Boot 4.1.1
|
||||||
|
stage s1 legacy; sed -i 's/3.5.16/4.1.1/; s/<java.version>21/<java.version>25/' "$W/s1/pom.xml"
|
||||||
|
{ echo '$ mvn compile # legacy sources, only the Boot version changed to 4.1.1 (Batch 6.0.5)'
|
||||||
|
mv_ "$J25" "$W/s1" compile > "$W/s1.log"
|
||||||
|
echo "compile error lines reported: $(grep -c '^\[ERROR\] .*\.java' "$W/s1.log")"; echo; echo 'packages that no longer exist:'
|
||||||
|
grep -oE 'package [a-z.]+ does not exist' "$W/s1.log" | sort -u; echo; echo 'classes that moved:'
|
||||||
|
grep -oE 'symbol: +class [A-Za-z]+' "$W/s1.log" | awk '{print $3}' | sort -u | tr '\n' ' '; echo; } > "$OUT/02-compile-errors-after-bump.txt"
|
||||||
|
# 03 imports ported, dependencies untouched
|
||||||
|
cp -r "$W/s1" "$W/s2"; rm -rf "$W/s2/target"; python3 -c "$PORT_IMPORTS" "$W/s2"
|
||||||
|
{ echo '$ mvn package # imports ported to the new packages, dependencies unchanged (spring-boot-starter-batch)'
|
||||||
|
mv_ "$J25" "$W/s2" -DskipTests package | grep -E 'WARNING.*\.java|BUILD|ERROR.*\.java'
|
||||||
|
echo; echo '$ java -jar target/batch-legacy-1.0.0.jar'
|
||||||
|
JAVA_HOME=$J25 $J25/bin/java -jar "$W/s2/target/batch-legacy-1.0.0.jar" 2>&1 | grep -A6 'APPLICATION FAILED'; } > "$OUT/03-imports-ported-no-transaction-manager.txt"
|
||||||
|
# 04 + JDBC starter
|
||||||
|
cp -r "$W/s2" "$W/s3"; rm -rf "$W/s3/target"; python3 - "$W/s3/pom.xml" <<'PY'
|
||||||
|
import sys; p=sys.argv[1]; s=open(p).read()
|
||||||
|
s=s.replace('<artifactId>spring-boot-starter-batch</artifactId></dependency>','<artifactId>spring-boot-starter-batch-jdbc</artifactId></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency>')
|
||||||
|
open(p,'w').write(s)
|
||||||
|
PY
|
||||||
|
{ echo '$ mvn package # + spring-boot-starter-batch-jdbc and spring-boot-starter-jdbc'
|
||||||
|
mv_ "$J25" "$W/s3" package | grep -E 'WARNING.*\.java|Tests run:|BUILD'
|
||||||
|
echo; echo '$ java -jar target/batch-legacy-1.0.0.jar'; runjar "$J25" "$W/s3/target/batch-legacy-1.0.0.jar"; } > "$OUT/04-jdbc-starter-added.txt"
|
||||||
|
# 05 new chunk API but old listener
|
||||||
|
cp -r "$W/s3" "$W/s4"; rm -rf "$W/s4/target"; sed -i 's/\.<Integer, String>chunk(3, tx)/.<Integer, String>chunk(3)\n .transactionManager(tx)/' "$W/s4/src/main/java/com/ankurm/batch/BatchConfig.java"
|
||||||
|
{ echo '$ mvn package # chunk(3, tx) replaced by chunk(3).transactionManager(tx); listener untouched'
|
||||||
|
mv_ "$J25" "$W/s4" -DskipTests package | grep -E 'WARNING.*\.java|BUILD|ERROR.*\.java'
|
||||||
|
echo; echo '$ java -jar target/batch-legacy-1.0.0.jar'; runjar "$J25" "$W/s4/target/batch-legacy-1.0.0.jar"; } > "$OUT/05-new-chunk-api-listener-silent.txt"
|
||||||
|
# 06 final migrated
|
||||||
|
stage m migrated
|
||||||
|
{ echo '$ mvn package # final migrated project'
|
||||||
|
mv_ "$J25" "$W/m" package | grep -E 'WARNING.*\.java|Tests run:|BUILD'
|
||||||
|
echo; echo '$ java -jar target/batch-migrated-1.0.0.jar'; runjar "$J25" "$W/m/target/batch-migrated-1.0.0.jar"; } > "$OUT/06-migrated.txt"
|
||||||
|
# 07 API facts from the jars
|
||||||
|
M2=${M2:-$HOME/.m2/repository/org/springframework/batch}
|
||||||
|
CP=$M2/spring-batch-core/6.0.5/spring-batch-core-6.0.5.jar:$M2/spring-batch-infrastructure/6.0.5/spring-batch-infrastructure-6.0.5.jar:$M2/spring-batch-test/6.0.5/spring-batch-test-6.0.5.jar
|
||||||
|
{ echo '$ javap (Batch 6.0.5 jars), selected lines'
|
||||||
|
for c in org.springframework.batch.core.launch.JobOperator org.springframework.batch.core.repository.JobRepository org.springframework.batch.test.JobOperatorTestUtils; do env -u JAVA_TOOL_OPTIONS $J25/bin/javap -cp $CP $c 2>&1 | grep -E '^public (interface|class)' | sed 's/org\.springframework\.batch\.//g'; done
|
||||||
|
env -u JAVA_TOOL_OPTIONS $J25/bin/javap -cp $CP org.springframework.batch.core.step.builder.StepBuilder 2>&1 | grep -E ' chunk\(' | sed 's/org\.springframework\.batch\.//g; s/org\.springframework\.transaction\.//g; s/org\.springframework\.//g'
|
||||||
|
env -u JAVA_TOOL_OPTIONS $J25/bin/javap -cp $CP org.springframework.batch.core.listener.ChunkListener 2>&1 | grep -E 'Chunk(Context|<)' | sed 's/org\.springframework\.batch\.//g'; } > "$OUT/07-api-facts.txt"
|
||||||
|
echo done
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# boot42-preview
|
||||||
|
|
||||||
|
Companion module for the ankurm.com article *Spring Boot 4.2 Preview: AMQP 1.0, Buildpack Image Cache and What to Test Now*.
|
||||||
|
Everything runs against **4.2.0-M2** (a milestone, not GA) and, where a comparison matters, against **4.1.1** (GA).
|
||||||
|
|
||||||
|
| Path | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `pom.xml` / `pom-41.xml` | the same project on 4.2.0-M2 and on 4.1.1 (a `${revision}` parent does not resolve from a repository, hence two files) |
|
||||||
|
| `src/test/.../ForwardedHeadersTest` | what the container believes under each forwarded-header strategy |
|
||||||
|
| `src/test/.../BindingOrderTest` | Set and Map iteration order from a properties file |
|
||||||
|
| `src/broken/UsesRestTemplate.java` | not built; compiled by the facts script to capture the deprecation warnings |
|
||||||
|
| `src/main/.../RestClientReplacement` | the replacement, compiled clean |
|
||||||
|
| `examples/build-image-cache-fragment.xml` | **not executed** (needs Docker and a registry) |
|
||||||
|
| `output/` | every transcript quoted in the article; `./scripts/run-all.sh` regenerates them |
|
||||||
|
|
||||||
|
There is deliberately no `docs/` folder: the explanations live in the article.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!-- NOT EXECUTED: needs a Docker daemon and a registry. Element names come from the setters javap shows in
|
||||||
|
output/04-buildpack-cache-classes.txt (CacheInfo.setImage, ImageCacheInfo.setName). -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<image>
|
||||||
|
<buildCache>
|
||||||
|
<image>
|
||||||
|
<name>registry.example.com/team/demo-build-cache:latest</name>
|
||||||
|
</image>
|
||||||
|
</buildCache>
|
||||||
|
</image>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 01-forwarded-headers (Spring Boot 4.1.1, JDK 25)
|
||||||
|
none/x-forwarded scheme=http secure=false host=localhost port=<random>
|
||||||
|
none/rfc7239 scheme=http secure=false host=localhost port=<random>
|
||||||
|
native/x-forwarded scheme=https secure=true host=shop.example.com port=443
|
||||||
|
native/rfc7239 scheme=http secure=false host=localhost port=<random>
|
||||||
|
framework/x-forwarded scheme=https secure=true host=shop.example.com port=443
|
||||||
|
framework/rfc7239 scheme=https secure=true host=shop.example.com port=443
|
||||||
|
framework+standard/x-forwarded scheme=https secure=true host=shop.example.com port=443
|
||||||
|
framework+standard/rfc7239 scheme=https secure=true host=shop.example.com port=443
|
||||||
|
header-format=forwarded -> started (property ignored)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 01-forwarded-headers (Spring Boot 4.2.0-M2, JDK 25)
|
||||||
|
none/x-forwarded scheme=http secure=false host=localhost port=<random>
|
||||||
|
none/rfc7239 scheme=http secure=false host=localhost port=<random>
|
||||||
|
native/x-forwarded scheme=https secure=true host=shop.example.com port=443
|
||||||
|
native/rfc7239 scheme=http secure=false host=localhost port=<random>
|
||||||
|
framework/x-forwarded scheme=https secure=true host=shop.example.com port=443
|
||||||
|
framework/rfc7239 scheme=http secure=false host=localhost port=<random>
|
||||||
|
framework+standard/x-forwarded scheme=http secure=false host=localhost port=<random>
|
||||||
|
framework+standard/rfc7239 scheme=https secure=true host=shop.example.com port=443
|
||||||
|
header-format=forwarded -> IllegalArgumentException: No enum constant org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.HeaderFormat.forwarded
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 02-set-and-map-binding-order (Spring Boot 4.1.1, JDK 25)
|
||||||
|
file order : zulu, alpha, mike, bravo, yankee, charlie
|
||||||
|
Set class : java.util.LinkedHashSet
|
||||||
|
Set order : [zulu, alpha, mike, bravo, yankee, charlie]
|
||||||
|
Map class : java.util.LinkedHashMap
|
||||||
|
Map key order : [zulu, alpha, mike, bravo, yankee, charlie]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 02-set-and-map-binding-order (Spring Boot 4.2.0-M2, JDK 25)
|
||||||
|
file order : zulu, alpha, mike, bravo, yankee, charlie
|
||||||
|
Set class : java.util.LinkedHashSet
|
||||||
|
Set order : [zulu, alpha, mike, bravo, yankee, charlie]
|
||||||
|
Map class : java.util.LinkedHashMap
|
||||||
|
Map key order : [zulu, alpha, mike, bravo, yankee, charlie]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# 03-amqp-starters (POM descriptions and contents straight from Maven Central)
|
||||||
|
spring-boot-starter-amqp 4.1.1 HTTP 200 "Starter for using Spring AMQP and Rabbit MQ" -> spring-boot-amqp
|
||||||
|
spring-boot-starter-amqp 4.2.0-M2 HTTP 200 "Starter for generic AMQP 1.0 support" -> spring-boot-amqp
|
||||||
|
spring-boot-starter-rabbitmq 4.1.1 HTTP 404 (does not exist)
|
||||||
|
spring-boot-starter-rabbitmq 4.2.0-M2 HTTP 200 "Starter for using RabbitMQ messaging and streaming broker" -> spring-boot-rabbitmq
|
||||||
|
spring-boot-starter-amqp-rabbitmq 4.1.1 HTTP 404 (does not exist)
|
||||||
|
spring-boot-starter-amqp-rabbitmq 4.2.0-M2 HTTP 200 "Starter for using Spring AMQP with Rabbit MQ over AMQP 1.0 protocol" -> spring-boot-amqp-rabbitmq
|
||||||
|
|
||||||
|
# auto-configuration each module registers (AutoConfiguration.imports)
|
||||||
|
--- spring-boot-amqp 4.1.1
|
||||||
|
org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration
|
||||||
|
org.springframework.boot.amqp.autoconfigure.health.RabbitHealthContributorAutoConfiguration
|
||||||
|
org.springframework.boot.amqp.autoconfigure.metrics.RabbitMetricsAutoConfiguration
|
||||||
|
--- spring-boot-amqp 4.2.0-M2
|
||||||
|
org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration
|
||||||
|
|
||||||
|
--- spring-boot-rabbitmq 4.2.0-M2
|
||||||
|
org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration
|
||||||
|
org.springframework.boot.rabbitmq.autoconfigure.health.RabbitHealthContributorAutoConfiguration
|
||||||
|
org.springframework.boot.rabbitmq.autoconfigure.metrics.RabbitMetricsAutoConfiguration
|
||||||
|
--- spring-boot-amqp-rabbitmq 4.2.0-M2
|
||||||
|
org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration
|
||||||
|
org.springframework.boot.amqp.rabbitmq.autoconfigure.health.AmqpRabbitHealthContributorAutoConfiguration
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 04-buildpack-cache-classes (javap, spring-boot-buildpack-platform and spring-boot-maven-plugin)
|
||||||
|
--- buildpack-platform 4.1.1: classes matching Cache
|
||||||
|
Cache$Bind.class
|
||||||
|
Cache$Format.class
|
||||||
|
Cache$Volume.class
|
||||||
|
Cache.class
|
||||||
|
--- buildpack-platform 4.2.0-M2: classes matching Cache
|
||||||
|
Cache.class
|
||||||
|
ImageCache.class
|
||||||
|
LocalCache$Bind.class
|
||||||
|
LocalCache$Volume.class
|
||||||
|
LocalCache.class
|
||||||
|
--- javap Cache (4.2.0-M2)
|
||||||
|
Compiled from "Cache.java"
|
||||||
|
public interface Cache {
|
||||||
|
public default LocalCache$Volume getVolume();
|
||||||
|
public default LocalCache$Bind getBind();
|
||||||
|
public default ImageCache getImage();
|
||||||
|
public static LocalCache volume(java.lang.String);
|
||||||
|
public static LocalCache volume(VolumeName);
|
||||||
|
public static LocalCache bind(java.lang.String);
|
||||||
|
public static ImageCache image(java.lang.String);
|
||||||
|
}
|
||||||
|
--- javap org.springframework.boot.maven.CacheInfo (4.2.0-M2)
|
||||||
|
Compiled from "CacheInfo.java"
|
||||||
|
public class CacheInfo {
|
||||||
|
public CacheInfo();
|
||||||
|
public void setVolume(LocalCacheInfo$VolumeCacheInfo);
|
||||||
|
public void setBind(LocalCacheInfo$BindCacheInfo);
|
||||||
|
public void setImage(CacheInfo$ImageCacheInfo);
|
||||||
|
Cache asCache();
|
||||||
|
static CacheInfo fromVolume(LocalCacheInfo$VolumeCacheInfo);
|
||||||
|
static CacheInfo fromBind(LocalCacheInfo$BindCacheInfo);
|
||||||
|
static CacheInfo fromImage(CacheInfo$ImageCacheInfo);
|
||||||
|
}
|
||||||
|
--- javap org.springframework.boot.maven.LocalCacheInfo (4.2.0-M2)
|
||||||
|
Compiled from "LocalCacheInfo.java"
|
||||||
|
public class LocalCacheInfo {
|
||||||
|
public LocalCacheInfo();
|
||||||
|
LocalCacheInfo(LocalCache);
|
||||||
|
public void setVolume(LocalCacheInfo$VolumeCacheInfo);
|
||||||
|
public void setBind(LocalCacheInfo$BindCacheInfo);
|
||||||
|
LocalCache asCache();
|
||||||
|
static LocalCacheInfo fromVolume(LocalCacheInfo$VolumeCacheInfo);
|
||||||
|
static LocalCacheInfo fromBind(LocalCacheInfo$BindCacheInfo);
|
||||||
|
}
|
||||||
|
--- javap -p Image, fields mentioning cache/workspace (4.2.0-M2)
|
||||||
|
LocalCacheInfo buildWorkspace;
|
||||||
|
CacheInfo buildCache;
|
||||||
|
LocalCacheInfo launchCache;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 05-resttemplate-deprecations (javac -Xlint:deprecation on src/broken/UsesRestTemplate.java)
|
||||||
|
--- Spring Boot 4.1.1
|
||||||
|
--- Spring Boot 4.2.0-M2
|
||||||
|
UsesRestTemplate.java:8: warning: [removal] RestTemplate in org.springframework.web.client has been deprecated and marked for removal
|
||||||
|
RestTemplate viaBuilder(RestTemplateBuilder builder) {
|
||||||
|
^
|
||||||
|
UsesRestTemplate.java:8: warning: [removal] RestTemplateBuilder in org.springframework.boot.restclient has been deprecated and marked for removal
|
||||||
|
RestTemplate viaBuilder(RestTemplateBuilder builder) {
|
||||||
|
^
|
||||||
|
UsesRestTemplate.java:12: warning: [removal] TestRestTemplate in org.springframework.boot.resttestclient has been deprecated and marked for removal
|
||||||
|
TestRestTemplate viaTest(TestRestTemplate t) {
|
||||||
|
^
|
||||||
|
UsesRestTemplate.java:12: warning: [removal] TestRestTemplate in org.springframework.boot.resttestclient has been deprecated and marked for removal
|
||||||
|
TestRestTemplate viaTest(TestRestTemplate t) {
|
||||||
|
^
|
||||||
|
UsesRestTemplate.java:16: warning: [removal] RestTemplate in org.springframework.web.client has been deprecated and marked for removal
|
||||||
|
RestTemplate plain() {
|
||||||
|
^
|
||||||
|
UsesRestTemplate.java:17: warning: [removal] RestTemplate in org.springframework.web.client has been deprecated and marked for removal
|
||||||
|
return new RestTemplate();
|
||||||
|
^
|
||||||
|
6 warnings
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# 06-restclient-replacement (javac -Xlint:deprecation on the RestClient version, Spring Boot 4.2.0-M2)
|
||||||
|
javac finished; lines above this one are warnings (none means a clean compile)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?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>boot42-preview-on-41</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>boot42-preview</name>
|
||||||
|
<description>Spring Boot 4.2 milestone preview: what to test now, run against 4.2.0-M2 and 4.1.1</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
<boot.label>4.1.1</boot.label>
|
||||||
|
</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-restclient</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<systemPropertyVariables>
|
||||||
|
<boot.label>${boot.label}</boot.label>
|
||||||
|
</systemPropertyVariables>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?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.2.0-M2</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>boot42-preview</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>boot42-preview</name>
|
||||||
|
<description>Spring Boot 4.2 milestone preview: what to test now, run against 4.2.0-M2 and 4.1.1</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
<boot.label>4.2.0-M2</boot.label>
|
||||||
|
</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-restclient</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<systemPropertyVariables>
|
||||||
|
<boot.label>${boot.label}</boot.label>
|
||||||
|
</systemPropertyVariables>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Executable
+78
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Facts read from jars and POMs on Maven Central: nothing here needs a running application.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
M="$HOME/.m2/repository/org/springframework/boot"
|
||||||
|
CEN=https://repo1.maven.org/maven2/org/springframework/boot
|
||||||
|
W=$(mktemp -d)
|
||||||
|
for v in 4.1.1 4.2.0-M2; do for a in spring-boot-maven-plugin spring-boot-buildpack-platform spring-boot-amqp spring-boot-amqp-rabbitmq spring-boot-rabbitmq; do
|
||||||
|
mvn -B -q dependency:get -Dartifact=org.springframework.boot:$a:$v >/dev/null 2>&1 || true
|
||||||
|
done; done
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# 03-amqp-starters (POM descriptions and contents straight from Maven Central)"
|
||||||
|
for a in spring-boot-starter-amqp spring-boot-starter-rabbitmq spring-boot-starter-amqp-rabbitmq; do
|
||||||
|
for v in 4.1.1 4.2.0-M2; do
|
||||||
|
for try in 1 2 3 4 5; do
|
||||||
|
code=$(curl -s -o "$W/p.xml" -w '%{http_code}' "$CEN/$a/$v/$a-$v.pom")
|
||||||
|
[ "$code" = 429 ] || break
|
||||||
|
sleep $((try * 3))
|
||||||
|
done
|
||||||
|
if [ "$code" = 200 ]; then
|
||||||
|
d=$(grep -m1 '<description>' "$W/p.xml" | sed 's/ *<[^>]*>//g')
|
||||||
|
dep=$(grep -E '<artifactId>spring-boot-(amqp|rabbitmq|amqp-rabbitmq)</artifactId>' "$W/p.xml" | sed 's/ *<[^>]*>//g' | paste -sd, -)
|
||||||
|
printf '%-34s %-10s HTTP %s "%s" -> %s\n' "$a" "$v" "$code" "$d" "$dep"
|
||||||
|
else
|
||||||
|
printf '%-34s %-10s HTTP %s (does not exist)\n' "$a" "$v" "$code"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "# auto-configuration each module registers (AutoConfiguration.imports)"
|
||||||
|
for a in spring-boot-amqp spring-boot-rabbitmq spring-boot-amqp-rabbitmq; do
|
||||||
|
for v in 4.1.1 4.2.0-M2; do
|
||||||
|
j="$M/$a/$v/$a-$v.jar"
|
||||||
|
if [ -f "$j" ]; then
|
||||||
|
echo "--- $a $v"; unzip -p "$j" META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2>/dev/null | sed 's/^/ /' || true; echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
} > output/03-amqp-starters.txt
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# 04-buildpack-cache-classes (javap, spring-boot-buildpack-platform and spring-boot-maven-plugin)"
|
||||||
|
for v in 4.1.1 4.2.0-M2; do
|
||||||
|
echo "--- buildpack-platform $v: classes matching Cache"
|
||||||
|
unzip -l "$M/spring-boot-buildpack-platform/$v/spring-boot-buildpack-platform-$v.jar" | awk '{print $4}' | grep -E 'build/.*Cache' | sed 's#.*/build/# #'
|
||||||
|
done
|
||||||
|
mkdir -p "$W/bp"
|
||||||
|
unzip -q -o "$M/spring-boot-buildpack-platform/4.2.0-M2/spring-boot-buildpack-platform-4.2.0-M2.jar" 'org/springframework/boot/buildpack/platform/build/*Cache*' -d "$W/bp"
|
||||||
|
unzip -q -o "$M/spring-boot-maven-plugin/4.2.0-M2/spring-boot-maven-plugin-4.2.0-M2.jar" 'org/springframework/boot/maven/*' -d "$W/bp"
|
||||||
|
echo "--- javap Cache (4.2.0-M2)"
|
||||||
|
javap -cp "$W/bp" org.springframework.boot.buildpack.platform.build.Cache 2>/dev/null | sed 's/org.springframework.boot.buildpack.platform.build.//g; s/org.springframework.boot.buildpack.platform.docker.type.//g'
|
||||||
|
for c in 'org.springframework.boot.maven.CacheInfo' 'org.springframework.boot.maven.LocalCacheInfo'; do
|
||||||
|
echo "--- javap $c (4.2.0-M2)"
|
||||||
|
javap -cp "$W/bp" "$c" 2>/dev/null | sed 's/org.springframework.boot.maven.//g; s/org.springframework.boot.buildpack.platform.build.//g'
|
||||||
|
done
|
||||||
|
echo "--- javap -p Image, fields mentioning cache/workspace (4.2.0-M2)"
|
||||||
|
javap -p -cp "$W/bp" org.springframework.boot.maven.Image 2>/dev/null | grep -E '(Cache|buildWorkspace)[a-zA-Z]* [a-zA-Z]+;$' | sed 's/org.springframework.boot.maven.//g'
|
||||||
|
} > output/04-buildpack-cache-classes.txt
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# 05-resttemplate-deprecations (javac -Xlint:deprecation on src/broken/UsesRestTemplate.java)"
|
||||||
|
for pom in pom-41.xml pom.xml; do
|
||||||
|
mvn -B -q -f $pom dependency:build-classpath -Dmdep.outputFile="$W/cp.txt" >/dev/null 2>&1
|
||||||
|
label=$([ $pom = pom.xml ] && echo 4.2.0-M2 || echo 4.1.1)
|
||||||
|
echo "--- Spring Boot $label"
|
||||||
|
mkdir -p "$W/out-$label"
|
||||||
|
javac -Xlint:deprecation -d "$W/out-$label" -cp "$(cat "$W/cp.txt")" src/broken/UsesRestTemplate.java 2>&1 | grep -v JAVA_TOOL | sed 's#^src/broken/##' || true
|
||||||
|
done
|
||||||
|
} > output/05-resttemplate-deprecations.txt
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# 06-restclient-replacement (javac -Xlint:deprecation on the RestClient version, Spring Boot 4.2.0-M2)"
|
||||||
|
mvn -B -q dependency:build-classpath -Dmdep.outputFile="$W/cp.txt" >/dev/null 2>&1
|
||||||
|
mkdir -p "$W/out-rc"
|
||||||
|
javac -Xlint:deprecation -Xlint:removal -d "$W/out-rc" -cp "$(cat "$W/cp.txt")" src/main/java/com/ankurm/boot42/RestClientReplacement.java 2>&1 | grep -v JAVA_TOOL | sed 's#^src/main/java/com/ankurm/boot42/##' || true
|
||||||
|
echo "javac finished; lines above this one are warnings (none means a clean compile)"
|
||||||
|
} > output/06-restclient-replacement.txt
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file under output/. Needs JDK 25 and Maven 3.9 with network access to Maven Central.
|
||||||
|
# ./scripts/run-all.sh
|
||||||
|
# 01-02 come from the test suite (run on 4.2.0-M2, then on 4.1.1 via pom-41.xml, which writes the *-on-4.1.1 files).
|
||||||
|
# 03-06 come from jars, POMs and javac. Port numbers are random, so the transcripts print "<random>".
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mvn -B test
|
||||||
|
mvn -B -f pom-41.xml test
|
||||||
|
./scripts/capture-facts.sh
|
||||||
|
ls -1 output
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import org.springframework.boot.restclient.RestTemplateBuilder;
|
||||||
|
import org.springframework.boot.resttestclient.TestRestTemplate;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/** Not part of the build: compiled by scripts/capture-facts.sh so the compiler's own words are committed. */
|
||||||
|
class UsesRestTemplate {
|
||||||
|
|
||||||
|
RestTemplate viaBuilder(RestTemplateBuilder builder) {
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
TestRestTemplate viaTest(TestRestTemplate t) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
RestTemplate plain() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/** Minimal application. Every experiment in this module lives in src/test. */
|
||||||
|
@SpringBootApplication
|
||||||
|
@org.springframework.boot.context.properties.EnableConfigurationProperties(OrderProperties.class)
|
||||||
|
public class Boot42PreviewApplication {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/** A Set and a Map bound from a file where the keys are deliberately not alphabetical. */
|
||||||
|
@ConfigurationProperties("app")
|
||||||
|
public record OrderProperties(Set<String> regions, Map<String, Integer> limits) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
/** What the deprecated RestTemplateBuilder code turns into: an injected RestClient.Builder. */
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
class RestClientReplacement {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RestClient inventoryClient(RestClient.Builder builder) {
|
||||||
|
return builder.baseUrl("http://inventory.internal").build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/** Echoes what the servlet container believes about the request, after any forwarded-header handling. */
|
||||||
|
@RestController
|
||||||
|
class WhoAmIController {
|
||||||
|
|
||||||
|
@GetMapping("/whoami")
|
||||||
|
Map<String, Object> whoAmI(HttpServletRequest request) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("scheme", request.getScheme());
|
||||||
|
m.put("secure", request.isSecure());
|
||||||
|
m.put("serverName", request.getServerName());
|
||||||
|
m.put("serverPort", request.getServerPort());
|
||||||
|
m.put("remoteAddr", request.getRemoteAddr());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
app.regions=zulu,alpha,mike,bravo,yankee,charlie
|
||||||
|
app.limits.zulu=1
|
||||||
|
app.limits.alpha=2
|
||||||
|
app.limits.mike=3
|
||||||
|
app.limits.bravo=4
|
||||||
|
app.limits.yankee=5
|
||||||
|
app.limits.charlie=6
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
/** 4.2.0-M1: Sets and Maps bind consistently and keep the order of the configuration file. */
|
||||||
|
class BindingOrderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recordsIterationOrder() {
|
||||||
|
Transcript t = new Transcript("02-set-and-map-binding-order");
|
||||||
|
try (ConfigurableApplicationContext ctx = BootRun.start("spring.config.name=order")) {
|
||||||
|
OrderProperties p = ctx.getBean(OrderProperties.class);
|
||||||
|
t.line("file order : zulu, alpha, mike, bravo, yankee, charlie");
|
||||||
|
t.line("Set class : " + p.regions().getClass().getName());
|
||||||
|
t.line("Set order : " + new ArrayList<>(p.regions()));
|
||||||
|
t.line("Map class : " + p.limits().getClass().getName());
|
||||||
|
t.line("Map key order : " + new ArrayList<>(p.limits().keySet()));
|
||||||
|
t.write();
|
||||||
|
assertThat(p.regions()).hasSize(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.boot.WebApplicationType;
|
||||||
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
/** Starts the real application with the given properties on a random port. */
|
||||||
|
final class BootRun {
|
||||||
|
|
||||||
|
private BootRun() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConfigurableApplicationContext start(String... props) {
|
||||||
|
return new SpringApplicationBuilder(Boot42PreviewApplication.class)
|
||||||
|
.web(WebApplicationType.SERVLET)
|
||||||
|
.properties(props)
|
||||||
|
.properties("server.port=0", "spring.main.banner-mode=off", "logging.level.root=WARN")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
static RestClient client(ConfigurableApplicationContext ctx) {
|
||||||
|
Environment env = ctx.getEnvironment();
|
||||||
|
return RestClient.create("http://localhost:" + env.getProperty("local.server.port"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<?, ?> whoAmI(ConfigurableApplicationContext ctx, Map<String, String> headers) {
|
||||||
|
return client(ctx).get().uri("/whoami").headers(h -> headers.forEach(h::add)).retrieve().body(Map.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4.2.0-M1: "When server.forward-headers-strategy=FRAMEWORK is selected, X-Forwarded-* headers are used by
|
||||||
|
* default." Sends both header styles under each strategy and records what the container believes.
|
||||||
|
*/
|
||||||
|
class ForwardedHeadersTest {
|
||||||
|
|
||||||
|
private static final Map<String, String> X_FORWARDED = Map.of("X-Forwarded-Proto", "https",
|
||||||
|
"X-Forwarded-Host", "shop.example.com", "X-Forwarded-Port", "443");
|
||||||
|
private static final Map<String, String> RFC_7239 = Map.of("Forwarded", "proto=https;host=shop.example.com");
|
||||||
|
|
||||||
|
private static final Map<String, Map<String, String>> HEADERS = new LinkedHashMap<>();
|
||||||
|
static {
|
||||||
|
HEADERS.put("x-forwarded", X_FORWARDED);
|
||||||
|
HEADERS.put("rfc7239", RFC_7239);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recordsBehaviourPerStrategy() {
|
||||||
|
Transcript t = new Transcript("01-forwarded-headers");
|
||||||
|
Map<String, Map<?, ?>> results = new LinkedHashMap<>();
|
||||||
|
for (String strategy : new String[] { "none", "native", "framework" }) {
|
||||||
|
for (var e : HEADERS.entrySet()) {
|
||||||
|
try (ConfigurableApplicationContext ctx = BootRun.start("server.forward-headers-strategy=" + strategy)) {
|
||||||
|
Map<?, ?> seen = BootRun.whoAmI(ctx, e.getValue());
|
||||||
|
String key = strategy + "/" + e.getKey();
|
||||||
|
results.put(key, seen);
|
||||||
|
t.line(String.format("%-22s scheme=%s secure=%s host=%s port=%s", key, seen.get("scheme"),
|
||||||
|
seen.get("secure"), seen.get("serverName"), seen.get("serverPort").toString().equals("443") ? "443" : "<random>"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the new 4.2 knob: which header family the FRAMEWORK strategy reads
|
||||||
|
for (var e : HEADERS.entrySet()) {
|
||||||
|
try (ConfigurableApplicationContext ctx = BootRun.start("server.forward-headers-strategy=framework",
|
||||||
|
"spring.mvc.forwarded-headers.header-format=standard")) {
|
||||||
|
Map<?, ?> seen = BootRun.whoAmI(ctx, e.getValue());
|
||||||
|
t.line(String.format("%-22s scheme=%s secure=%s host=%s port=%s", "framework+standard/" + e.getKey(),
|
||||||
|
seen.get("scheme"), seen.get("secure"), seen.get("serverName"), seen.get("serverPort").toString().equals("443") ? "443" : "<random>"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// my first guess at the value was "forwarded"; record what the binder says about it
|
||||||
|
try (ConfigurableApplicationContext ctx = BootRun.start("server.forward-headers-strategy=framework",
|
||||||
|
"spring.mvc.forwarded-headers.header-format=forwarded")) {
|
||||||
|
t.line("header-format=forwarded -> started (property ignored)");
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Throwable root = ex;
|
||||||
|
while (root.getCause() != null) {
|
||||||
|
root = root.getCause();
|
||||||
|
}
|
||||||
|
t.line("header-format=forwarded -> " + root.getClass().getSimpleName() + ": " + root.getMessage());
|
||||||
|
}
|
||||||
|
t.write();
|
||||||
|
// a strategy of "none" must never trust either header
|
||||||
|
assertThat(results.get("none/x-forwarded").get("scheme")).isEqualTo("http");
|
||||||
|
assertThat(results.get("none/rfc7239").get("scheme")).isEqualTo("http");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ankurm.boot42;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Collects lines and writes output/NN-name.txt. The test asserts the same values it writes. */
|
||||||
|
final class Transcript {
|
||||||
|
|
||||||
|
private final List<String> lines = new ArrayList<>();
|
||||||
|
private final Path file;
|
||||||
|
|
||||||
|
Transcript(String name) {
|
||||||
|
String v = System.getProperty("boot.label", "4.2.0-M2");
|
||||||
|
String suffix = v.equals("4.2.0-M2") ? "" : "-on-" + v;
|
||||||
|
this.file = Path.of("output", name + suffix + ".txt");
|
||||||
|
line("# " + name + " (Spring Boot " + v + ", JDK " + Runtime.version().feature() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
Transcript line(String s) {
|
||||||
|
lines.add(s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
Files.write(file, lines);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
*.class
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# core-beans
|
||||||
|
|
||||||
|
Companion project for three articles on **[ankurm.com](https://ankurm.com)**.
|
||||||
|
|
||||||
|
| Article | What it demonstrates |
|
||||||
|
|---|---|
|
||||||
|
| [Spring Bean Scopes: Singleton, Prototype, Request, Session and the Prototype-in-Singleton Trap](https://ankurm.com/spring-bean-scopes-singleton-prototype-request-session-prototype-in-singleton-trap/) | instance counts for every scope, five ways to get a fresh prototype inside a singleton, a singleton that leaks one caller's data into another's, and request scope without a proxy failing at start-up |
|
||||||
|
| [Spring Bean Lifecycle in Boot 4: @PostConstruct, InitializingBean, SmartLifecycle and Shutdown Order](https://ankurm.com/spring-bean-lifecycle-postconstruct-smartlifecycle-shutdown-order/) | every callback in order from a real run, `@PostConstruct` running before the proxy exists, the non-static `BeanPostProcessor` warning, `SmartLifecycle` phases measured, and a request in flight during graceful shutdown with virtual threads |
|
||||||
|
| [Circular Dependencies in Spring Boot 4: Why Startup Fails and 4 Ways to Fix It](https://ankurm.com/spring-boot-4-circular-dependencies-startup-fails-4-fixes/) | the start-up failure report for a three-service ring, the creation order behind it, what `allow-circular-references` does and does not repair (a half-built bean, `@Async` versus `@Cacheable`, an INFO line that swallows an exception), and four fixes run side by side: redesign, events, `@Lazy`, `ObjectProvider` |
|
||||||
|
|
||||||
|
Every console block, exception message and count quoted in those articles came out of `output/`,
|
||||||
|
and every file there is regenerated by one script. Most are written by the test suite, so if a
|
||||||
|
claim stops being true the build goes red.
|
||||||
|
|
||||||
|
There is deliberately **no `docs/` folder**: the deeper material lives in collapsible "going
|
||||||
|
deeper" sections inside the articles themselves, next to the paragraph each one extends.
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Spring Boot | 4.1.1 |
|
||||||
|
| Spring Framework | 7.0.9 |
|
||||||
|
| JDK | 25 (Temurin 25.0.4.1+1) |
|
||||||
|
| Maven | 3.9 |
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JAVA_HOME=/path/to/jdk-25
|
||||||
|
mvn test # runs the scenarios and rewrites the test-written files in output/
|
||||||
|
./scripts/run-all.sh # everything, including the script-captured files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source layout
|
||||||
|
|
||||||
|
| Package | What it holds |
|
||||||
|
|---|---|
|
||||||
|
| `scopes/` | singleton, `@Lazy`, prototype, scoped-prototype and the five ways a singleton can obtain a prototype; the shared-state greeter |
|
||||||
|
| `web/` | `@RequestScope`, `@SessionScope`, `@ApplicationScope` beans and a diagnostic `/scopes` controller, plus the un-proxied request bean that fails at start-up |
|
||||||
|
| `lifecycle/` | `KitchenSink` (every callback), the post-processor traps, `PhasedWorker` and `NeverStops` for `SmartLifecycle` |
|
||||||
|
| `shutdown/` | a web application with a `/slow` endpoint and two `SmartLifecycle` beans at different phases |
|
||||||
|
| `cycles/` | one package per scenario: `broken/` (the three-service ring), `allowed/` (field cycles with the flag on), `redesign/`, `events/`, `lazy/` and `provider/` (the four fixes), `beanmethods/` (a cycle written as `@Bean` methods) |
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
Both endpoints exist only to be called by the tests. They have no authorisation. Delete them before shipping.
|
||||||
|
|
||||||
|
| Endpoint | Application | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET /scopes` | `WebScopesApp` | request, session and application instance serials, and the injected proxy class |
|
||||||
|
| `GET /slow?ms=1500` | `ShutdownApp` | sleeps, so a request can be in flight when the context closes |
|
||||||
|
|
||||||
|
## Captured output
|
||||||
|
|
||||||
|
Files 01-17 and 19-35 (tests) and 18 (`capture-metadata.sh`). Timing rows assert coarse thresholds, not exact milliseconds; treat them as indicative.
|
||||||
|
|
||||||
|
| File | What it shows |
|
||||||
|
|---|---|
|
||||||
|
| [`01-instance-counts.txt`](output/01-instance-counts.txt) | How many instances did the container really construct? |
|
||||||
|
| [`02-prototype-destroy.txt`](output/02-prototype-destroy.txt) | @PreDestroy on a singleton and on a prototype, then context.close() |
|
||||||
|
| [`03-prototype-in-singleton.txt`](output/03-prototype-in-singleton.txt) | A singleton calls use() five times. How many prototype instances did it touch? |
|
||||||
|
| [`04-singleton-thread-safety.txt`](output/04-singleton-thread-safety.txt) | Two threads call the same singleton; a latch forces the interleaving |
|
||||||
|
| [`05-web-scopes.txt`](output/05-web-scopes.txt) | request, session and application scope over real HTTP |
|
||||||
|
| [`06-request-scope-without-proxy.txt`](output/06-request-scope-without-proxy.txt) | A request-scoped bean (no proxy) injected into a singleton |
|
||||||
|
| [`07-full-callback-order.txt`](output/07-full-callback-order.txt) | Every callback for one bean, from constructor to the last destroy hook (SpringApplication, no web server) |
|
||||||
|
| [`08-non-static-bpp-warning.txt`](output/08-non-static-bpp-warning.txt) | A BeanPostProcessor declared with a non-static @Bean method |
|
||||||
|
| [`09-postconstruct-before-proxy.txt`](output/09-postconstruct-before-proxy.txt) | @PostConstruct runs before the @Async proxy exists |
|
||||||
|
| [`10-postconstruct-failure.txt`](output/10-postconstruct-failure.txt) | An exception thrown from @PostConstruct |
|
||||||
|
| [`11-smartlifecycle-phases.txt`](output/11-smartlifecycle-phases.txt) | Three SmartLifecycle beans registered in the order 300, 100, 200 |
|
||||||
|
| [`12-blocking-vs-async-stop.txt`](output/12-blocking-vs-async-stop.txt) | Three SmartLifecycle beans in the SAME phase, each needing 400 ms to stop |
|
||||||
|
| [`13-plain-lifecycle.txt`](output/13-plain-lifecycle.txt) | Lifecycle vs SmartLifecycle: who starts at refresh()? |
|
||||||
|
| [`14-shutdown-timeout.txt`](output/14-shutdown-timeout.txt) | A SmartLifecycle whose stop(callback) never calls the callback, timeout 500 ms |
|
||||||
|
| [`15-smartlifecycle-beans-in-boot.txt`](output/15-smartlifecycle-beans-in-boot.txt) | Every SmartLifecycle bean in a Boot web application, highest phase (stops first) at the top |
|
||||||
|
| [`16-graceful-shutdown-in-flight.txt`](output/16-graceful-shutdown-in-flight.txt) | A /slow?ms=1500 request is in flight when the context closes |
|
||||||
|
| [`17-worker-phase-vs-web-server.txt`](output/17-worker-phase-vs-web-server.txt) | SmartLifecycle beans with the default phase and with phase 1000, while a request is in flight |
|
||||||
|
| [`18-property-defaults.txt`](output/18-property-defaults.txt) | Property defaults read from spring-configuration-metadata.json (Boot 4.1.1 jars) |
|
||||||
|
| [`19-bean-post-processors.txt`](output/19-bean-post-processors.txt) | The BeanPostProcessors registered in a plain Spring Boot context, in the order they run |
|
||||||
|
| [`20-boot-cycle-failure-report.txt`](output/20-boot-cycle-failure-report.txt) | Three constructor-injected services in a ring: what Spring Boot prints when start-up fails |
|
||||||
|
| [`21-cycle-exception-and-creation-order.txt`](output/21-cycle-exception-and-creation-order.txt) | The exception behind the report, and the order in which the container started creating beans |
|
||||||
|
| [`22-bean-method-cycle-failure-report.txt`](output/22-bean-method-cycle-failure-report.txt) | A two-bean cycle written as `@Bean` methods |
|
||||||
|
| [`23-lazy-initialization-hides-the-cycle.txt`](output/23-lazy-initialization-hides-the-cycle.txt) | `spring.main.lazy-initialization=true` on the same ring: it starts, then fails on first use |
|
||||||
|
| [`24-allow-circular-half-built-bean.txt`](output/24-allow-circular-half-built-bean.txt) | A field cycle with the flag off and on: what each `@PostConstruct` sees |
|
||||||
|
| [`25-allow-circular-async-raw-reference.txt`](output/25-allow-circular-async-raw-reference.txt) | The same cycle with `@Async`: repaired silently at start-up, a raw-version error under lazy initialisation |
|
||||||
|
| [`26-allow-circular-cacheable-early-proxy.txt`](output/26-allow-circular-cacheable-early-proxy.txt) | The same cycle with `@Cacheable`: the injected reference is the proxy |
|
||||||
|
| [`27-fix-redesign.txt`](output/27-fix-redesign.txt) | Fix 1: extract the one thing the second bean needed |
|
||||||
|
| [`28-fix-events.txt`](output/28-fix-events.txt) | Fix 2: an event instead of a call, and what the publisher sees when the listener throws |
|
||||||
|
| [`29-fix-lazy.txt`](output/29-fix-lazy.txt) | Fix 3: `@Lazy` on the parameter, and the proxy class that arrives |
|
||||||
|
| [`30-fix-object-provider.txt`](output/30-fix-object-provider.txt) | Fix 4: `ObjectProvider`, resolving in the constructor, and a final class |
|
||||||
|
| [`31-lazy-on-a-final-class.txt`](output/31-lazy-on-a-final-class.txt) | `@Lazy` on a parameter whose type is a final class |
|
||||||
|
| [`32-lazy-injection-point-versus-lazy-bean.txt`](output/32-lazy-injection-point-versus-lazy-bean.txt) | `@Lazy` on the injection point versus on the bean, with a target that cannot be built |
|
||||||
|
| [`33-dispatcher-handler-cycle.txt`](output/33-dispatcher-handler-cycle.txt) | A cycle through `List<Handler>` and its `ObjectProvider` repair |
|
||||||
|
| [`34-depends-on-cycle.txt`](output/34-depends-on-cycle.txt) | `@DependsOn` in both directions: no injection involved |
|
||||||
|
| [`35-lazy-used-during-construction.txt`](output/35-lazy-used-during-construction.txt) | `@Lazy` on the parameter, but the bean calls the dependency from `@PostConstruct` |
|
||||||
|
| [`36-main-property-defaults.txt`](output/36-main-property-defaults.txt) | The defaults of `spring.main.allow-circular-references` and `spring.main.lazy-initialization`, from Boot's own metadata |
|
||||||
|
| [`37-early-reference-bytecode.txt`](output/37-early-reference-bytecode.txt) | Which post-processors hand out an early reference, and the `BeanCurrentlyInCreationException` handler in `preInstantiateSingleton` |
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
MIT, see the repository root.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# How many instances did the container really construct?
|
||||||
|
|
||||||
|
right after refresh():
|
||||||
|
singleton instances : 1
|
||||||
|
@Lazy singleton : 0
|
||||||
|
prototype : 0
|
||||||
|
|
||||||
|
after getBean() twice for singleton and lazy, three times for prototype:
|
||||||
|
singleton instances : 1 same object both times: true
|
||||||
|
@Lazy singleton : 1 same object both times: true
|
||||||
|
prototype : 3 serials: 1, 2, 3
|
||||||
|
|
||||||
|
two separate contexts, each with SingletonBean:
|
||||||
|
singleton instances : 2 same object across contexts: false
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# @PreDestroy on a singleton and on a prototype, then context.close()
|
||||||
|
|
||||||
|
prototype instances created: 1
|
||||||
|
callbacks after close(): [singleton @PreDestroy called]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# A singleton calls use() five times. How many prototype instances did it touch?
|
||||||
|
|
||||||
|
how the prototype is obtained serials seen instances built
|
||||||
|
---------------------------------- ---------------------- ---------------
|
||||||
|
constructor injection (naive) [1, 1, 1, 1, 1] 1
|
||||||
|
ObjectProvider.getObject() [1, 2, 3, 4, 5] 5
|
||||||
|
@Lookup method [1, 2, 3, 4, 5] 5
|
||||||
|
scoped proxy (TARGET_CLASS) [1, 2, 3, 4, 5] 5
|
||||||
|
ApplicationContext.getBean() [1, 2, 3, 4, 5] 5
|
||||||
|
|
||||||
|
ProxyConsumer's injected reference is a com.ankurm.corebeans.scopes.ScopedPrototypeBean$$SpringCGLIB$$0
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Two threads call the same singleton; a latch forces the interleaving
|
||||||
|
|
||||||
|
UnsafeGreeter (state in a field) : alice's call returned "Hello, bob"
|
||||||
|
SafeGreeter (state in a local) : alice's call returned "Hello, alice"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# request, session and application scope over real HTTP
|
||||||
|
|
||||||
|
client response
|
||||||
|
A #1 request=1 session=1 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
|
||||||
|
A #2 request=2 session=1 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
|
||||||
|
A #3 request=3 session=1 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
|
||||||
|
B #1 request=4 session=2 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
|
||||||
|
B #2 request=5 session=2 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
|
||||||
|
|
||||||
|
instances constructed: request=5 session=2 application=1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# A request-scoped bean (no proxy) injected into a singleton
|
||||||
|
|
||||||
|
top exception : org.springframework.beans.factory.UnsatisfiedDependencyException
|
||||||
|
top message : Error creating bean with name 'brokenRequestConsumer': Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'brokenRequestConsumer.RawRequestBean': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton
|
||||||
|
|
||||||
|
root cause : java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Every callback for one bean, from constructor to the last destroy hook (SpringApplication, no web server)
|
||||||
|
|
||||||
|
=== startup ===
|
||||||
|
constructor
|
||||||
|
setter injection (@Autowired setDependency)
|
||||||
|
BeanNameAware.setBeanName("kitchenSink")
|
||||||
|
BeanFactoryAware.setBeanFactory
|
||||||
|
ApplicationContextAware.setApplicationContext
|
||||||
|
BeanPostProcessor.postProcessBeforeInitialization
|
||||||
|
@PostConstruct
|
||||||
|
InitializingBean.afterPropertiesSet
|
||||||
|
@Bean(initMethod = "customInit")
|
||||||
|
BeanPostProcessor.postProcessAfterInitialization
|
||||||
|
SmartInitializingSingleton.afterSingletonsInstantiated
|
||||||
|
SmartLifecycle.start (phase 2147483647)
|
||||||
|
ContextRefreshedEvent
|
||||||
|
ApplicationStartedEvent
|
||||||
|
ApplicationRunner.run
|
||||||
|
ApplicationReadyEvent
|
||||||
|
|
||||||
|
=== ctx.close() ===
|
||||||
|
ContextClosedEvent
|
||||||
|
SmartLifecycle.stop
|
||||||
|
@PreDestroy
|
||||||
|
DisposableBean.destroy
|
||||||
|
@Bean(destroyMethod = "customDestroy")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# A BeanPostProcessor declared with a non-static @Bean method
|
||||||
|
|
||||||
|
2026-09-24T10:28:59.979+05:30 WARN 3358 --- [core-beans] [ main] trationDelegate$BeanPostProcessorChecker : Bean 'nonStaticProcessorConfig' of type [com.ankurm.corebeans.lifecycle.NonStaticProcessorConfig$$SpringCGLIB$$0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). The currently created BeanPostProcessor [quietProcessor] is declared through a non-static factory method on that class; consider declaring it as static instead.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# @PostConstruct runs before the @Async proxy exists
|
||||||
|
|
||||||
|
@PostConstruct sees this = com.ankurm.corebeans.lifecycle.AsyncMailer, isAopProxy(this) = false
|
||||||
|
the bean other code receives : com.ankurm.corebeans.lifecycle.AsyncMailer$$SpringCGLIB$$0
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# An exception thrown from @PostConstruct
|
||||||
|
|
||||||
|
context started: false
|
||||||
|
top exception : org.springframework.beans.factory.BeanCreationException
|
||||||
|
top message : Error creating bean with name 'failingInit': Invocation of init method failed
|
||||||
|
root cause : java.lang.IllegalStateException: cache warm-up failed
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Three SmartLifecycle beans registered in the order 300, 100, 200
|
||||||
|
|
||||||
|
=== refresh() ===
|
||||||
|
start A (phase 100)
|
||||||
|
start B (phase 200)
|
||||||
|
start C (phase 300)
|
||||||
|
=== close() ===
|
||||||
|
stop C (phase 300)
|
||||||
|
stop B (phase 200)
|
||||||
|
stop A (phase 100)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Three SmartLifecycle beans in the SAME phase, each needing 400 ms to stop
|
||||||
|
|
||||||
|
stop(Runnable) blocks (the default) : close() took >= 1100 ms (sequential)
|
||||||
|
stop(Runnable) returns immediately : close() took < 800 ms (concurrent)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Lifecycle vs SmartLifecycle: who starts at refresh()?
|
||||||
|
|
||||||
|
after refresh() : events=[] isRunning=false
|
||||||
|
after start() : events=[PlainLifecycle.start] isRunning=true
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# A SmartLifecycle whose stop(callback) never calls the callback, timeout 500 ms
|
||||||
|
|
||||||
|
close() returned after roughly the timeout: true
|
||||||
|
2026-09-24T10:28:58.263+05:30 INFO 3358 --- [core-beans] [ main] o.s.c.support.DefaultLifecycleProcessor : Shutdown phase 2147483647 ends with 1 bean still running after timeout of 500ms: [neverStops]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Every SmartLifecycle bean in a Boot web application, highest phase (stops first) at the top
|
||||||
|
|
||||||
|
phase bean name class
|
||||||
|
2147483647 earlyWorker EarlyWorker
|
||||||
|
2147482623 webServerGracefulShutdown WebServerGracefulShutdownLifecycle
|
||||||
|
2147481599 webServerStartStop WebServerStartStopLifecycle
|
||||||
|
1073741823 applicationTaskExecutor ThreadPoolTaskExecutor
|
||||||
|
1000 lateWorker LateWorker
|
||||||
|
-2147483647 springBootLoggingLifecycle Lifecycle
|
||||||
|
|
||||||
|
SmartLifecycle.DEFAULT_PHASE = 2147483647
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# A /slow?ms=1500 request is in flight when the context closes
|
||||||
|
|
||||||
|
settings the client saw close() blocked handler ran to the end
|
||||||
|
-------------------------------------------- ---------------------------------------------------- -------------------------------- ----------------------
|
||||||
|
server.shutdown=graceful, virtual threads HTTP 200: finished after 1500 ms, virtual thread = true >= 1 s (waited for the request) yes
|
||||||
|
server.shutdown=graceful, platform threads HTTP 200: finished after 1500 ms, virtual thread = false >= 1 s (waited for the request) yes
|
||||||
|
server.shutdown=immediate, virtual threads FAILED: connection dropped, no response >= 1 s (waited for the request) yes
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# SmartLifecycle beans with the default phase and with phase 1000, while a request is in flight
|
||||||
|
|
||||||
|
EarlyWorker.stop (phase 2147483647): requests still in flight = 1
|
||||||
|
LateWorker.stop (phase 1000): requests still in flight = 0
|
||||||
|
the client saw: HTTP 200: finished after 1500 ms, virtual thread = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Property defaults read from spring-configuration-metadata.json (Boot 4.1.1 jars)
|
||||||
|
|
||||||
|
spring.lifecycle.timeout-per-shutdown-phase type=java.time.Duration default=30s
|
||||||
|
spring.threads.virtual.enabled type=java.lang.Boolean default=False
|
||||||
|
server.shutdown type=org.springframework.boot.web.server.Shutdown default=graceful
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# The BeanPostProcessors registered in a plain Spring Boot context, in the order they run
|
||||||
|
|
||||||
|
1 org.springframework.context.support.ApplicationContextAwareProcessor
|
||||||
|
2 org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor
|
||||||
|
3 org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker
|
||||||
|
4 com.ankurm.corebeans.lifecycle.TracingPostProcessor
|
||||||
|
5 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
|
||||||
|
6 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
|
||||||
|
7 org.springframework.context.support.ApplicationListenerDetector
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Three constructor-injected services in a ring: what Spring Boot 4.1.1 prints when start-up fails
|
||||||
|
|
||||||
|
***************************
|
||||||
|
APPLICATION FAILED TO START
|
||||||
|
***************************
|
||||||
|
|
||||||
|
Description:
|
||||||
|
|
||||||
|
The dependencies of some of the beans in the application context form a cycle:
|
||||||
|
|
||||||
|
┌─────┐
|
||||||
|
| notificationService defined in file [<core-beans>/target/classes/com/ankurm/corebeans/cycles/broken/NotificationService.class]
|
||||||
|
↑ ↓
|
||||||
|
| orderService defined in file [<core-beans>/target/classes/com/ankurm/corebeans/cycles/broken/OrderService.class]
|
||||||
|
↑ ↓
|
||||||
|
| paymentService defined in file [<core-beans>/target/classes/com/ankurm/corebeans/cycles/broken/PaymentService.class]
|
||||||
|
└─────┘
|
||||||
|
|
||||||
|
|
||||||
|
Action:
|
||||||
|
|
||||||
|
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# The exception behind the report, and the order in which the container started creating beans
|
||||||
|
|
||||||
|
|
||||||
|
--- exception classes, outermost first ---
|
||||||
|
UnsatisfiedDependencyException -> UnsatisfiedDependencyException -> UnsatisfiedDependencyException -> BeanCurrentlyInCreationException
|
||||||
|
|
||||||
|
--- root cause ---
|
||||||
|
org.springframework.beans.factory.BeanCurrentlyInCreationException
|
||||||
|
Error creating bean with name 'notificationService': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
|
||||||
|
|
||||||
|
--- creation order (one line per bean the container began to create) ---
|
||||||
|
start creating notificationService
|
||||||
|
start creating orderService
|
||||||
|
start creating paymentService
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# A two-bean cycle written as @Bean methods: the report names the configuration class instead of a class file
|
||||||
|
|
||||||
|
***************************
|
||||||
|
APPLICATION FAILED TO START
|
||||||
|
***************************
|
||||||
|
|
||||||
|
Description:
|
||||||
|
|
||||||
|
The dependencies of some of the beans in the application context form a cycle:
|
||||||
|
|
||||||
|
┌─────┐
|
||||||
|
| clock defined in com.ankurm.corebeans.cycles.beanmethods.BeanMethodCycle
|
||||||
|
↑ ↓
|
||||||
|
| calendar defined in com.ankurm.corebeans.cycles.beanmethods.BeanMethodCycle
|
||||||
|
└─────┘
|
||||||
|
|
||||||
|
|
||||||
|
Action:
|
||||||
|
|
||||||
|
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user