skip to content

How does Spring Boot's training-run archive work, and what does -Dspring.context.exit=onRefresh do?

level: seniorimportance: should knowfreq 20%

answer

  1. onRefresh: refresh context then exit before serving
  2. ArchiveClassesAtExit captures the training-run classes
  3. Extract jar first for a stable classpath
  4. BP_JVM_CDS_ENABLED=true = buildpack does it at build time
  5. CDS + AOT stack on the JVM

basics

~10 s

-Dspring.context.exit=onRefresh tells Spring Boot to start the context, finish refreshing (all beans created, classes loaded), then exit the JVM before serving traffic. Run it with -XX:ArchiveClassesAtExit to capture those classes into a CDS archive.

solid answer

~40 s

Spring Boot 3.3+ formalizes the CDS 'training run'. The property `spring.context.exit=onRefresh` makes the application context proceed through full refresh — instantiating all non-lazy singletons, resolving autoconfiguration, loading the classes required to bootstrap — and then the JVM exits cleanly instead of blocking on the web server. Pair it with `-XX:ArchiveClassesAtExit=application.jsa` and the exit dumps exactly those startup classes into a dynamic CDS archive. In production you then run with `-XX:SharedArchiveFile=application.jsa`. Best practice is to run against the extracted jar layout (`java -Djarmode=tools -jar app.jar extract`) so the classpath is stable and matches production. The recommended zero-effort path is the Paketo/Spring Boot buildpack with `BP_JVM_CDS_ENABLED=true`, which performs the training run and bakes the archive into the image at build time. CDS composes with Spring AOT for additional startup gains.

code

java · 19 lines
java
// Gradle build-image with buildpack-automated CDS training run:
//   ./gradlew bootBuildImage \
//     --imageName=example/app \
//     -Dorg.gradle.jvmargs=... \
//     -PBP_JVM_CDS_ENABLED=true   // (usually set as a build env var)
//
// Or set the buildpack env directly:
//   BP_JVM_CDS_ENABLED=true ./mvnw spring-boot:build-image
//
// Beware: onRefresh actually instantiates eager singletons, so a bean
// like this runs its @PostConstruct during the TRAINING run too:
@org.springframework.stereotype.Component
class Warmup {
    @jakarta.annotation.PostConstruct
    void init() {
        // Runs at context refresh -> also executes during the CDS training run.
        // Avoid real external side effects here, or guard the training profile.
    }
}

go deeper

for a junior

Know onRefresh means 'start up then quit', used to make the archive.

for a middle

Explain that it refreshes the context and exits before serving, paired with ArchiveClassesAtExit.

for a senior

Automates via buildpack, uses the extracted layout, and knows onRefresh actually runs bean init (side-effect risk).

for a principal

Designs the CI pipeline (profile-isolated training run, archive baked into image, re-trained on dependency/JDK change) and combines CDS with AOT.

## Why a 'training run' at all Dynamic CDS archives the classes **actually loaded during a run**. To get a useful Spring Boot archive you must load the same classes production will need at startup — ideally without the noise and blocking of actually serving traffic. That controlled run is the **training run**. ## `spring.context.exit=onRefresh` This Spring Boot property (available from **Spring Boot 3.3**) changes the lifecycle: the `ApplicationContext` is created and **refreshed** — meaning autoconfiguration is evaluated, all eager singleton beans are instantiated, and the classes behind them are loaded — and then, instead of the normal 'keep running / bind the web server' step, the JVM **exits**. Because it exits *cleanly*, `-XX:ArchiveClassesAtExit` fires and writes the archive. Contrast with a plain `java -jar app.jar` training run: an embedded-server app boots Tomcat/Netty and blocks forever, so the JVM never exits and no archive is produced. `onRefresh` solves that deterministically and quickly. (The related value used elsewhere is that the context reaches refresh but doesn't advance to the running phase.) ## The end-to-end flow ``` # 1. Extract the fat jar into a stable, exploded classpath java -Djarmode=tools -jar app.jar extract --destination app cd app # 2. Training run: full context init, then exit -> dump archive java -Dspring.context.exit=onRefresh \ -XX:ArchiveClassesAtExit=application.jsa \ -jar app.jar # 3. Production: reuse the archive on every start java -XX:SharedArchiveFile=application.jsa -jar app.jar ``` ## Why the extracted layout matters CDS wants a **stable classpath** that matches between training and production. Running from the extracted layout (plain directories of classes/jars) rather than relying on nested-jar classloading gives a deterministic classpath and lets CDS map the most classes. Spring Boot's `-Djarmode=tools ... extract` produces exactly this. ## Buildpack automation (recommended in practice) The **Paketo / Spring Boot buildpacks** support `BP_JVM_CDS_ENABLED=true`. When set, the buildpack performs the training run at **image build time** and embeds `application.jsa` in the resulting container image, wiring the `-XX:SharedArchiveFile` flag automatically. You get CDS with no manual scripting: `./mvnw spring-boot:build-image` (or Gradle `bootBuildImage`) with the env set. ## CDS + AOT together CDS and **Spring AOT** are complementary. AOT (`spring-boot-maven-plugin` `process-aot`, or `-Dspring.aot.enabled=true` in the AOT-processed jar) pre-computes bean definitions and generates source at build time; CDS caches the *class-loading* work. Used together on the JVM they stack for a bigger startup reduction than either alone — without the closed-world limits of a full native image. ## Gotchas - The training run must **exit cleanly**; a crash or kill yields no archive. - **Re-run the training step whenever dependencies, the app jar, or the JDK change** — otherwise the archive under-covers or is rejected. - `onRefresh` runs your bean initialization logic (constructors, `@PostConstruct`), so beans that do real I/O at construction will execute during the training run — make sure that's acceptable (e.g., avoid firing external side effects, or profile the training run so it doesn't hit prod resources). - CDS archives startup-time classes; endpoints first exercised only under real traffic aren't in the archive unless the training run touched them.

  • Your training run hits a production database during context refresh. Why is that a concern and how do you avoid it?
    onRefresh instantiates eager singletons and runs @PostConstruct, so any I/O in bean init executes during training — potentially against real resources. Run the training step with a training/CI profile (mock or local datasources) or make side-effecting init lazy, so the archive is built without touching production.
  • Can you use CDS and Spring AOT at the same time?
    Yes — they are complementary. AOT pre-generates bean definitions and source at build time; CDS caches class-loading work. On the JVM they stack for greater startup reduction, and unlike native image you keep full runtime dynamism.
  • Why run against the extracted jar rather than the nested fat jar?
    CDS needs a stable classpath that matches production to map the most classes. The extracted (exploded) layout from java -Djarmode=tools -jar app.jar extract gives a deterministic file-based classpath instead of relying on Spring Boot's nested-jar classloader.

saying these in an interview costs you the question

  • Thinking onRefresh skips bean creation (it doesn't — eager singletons run)
  • Running the training step against production resources during context refresh
  • Believing a normal blocking `java -jar` run can produce the archive
  • Assuming CDS and AOT are mutually exclusive

context