Adds spring-boot-startup-time/, the companion project for BLOG-618: a runnable Spring Boot 4.1.1 application on JDK 25 that installs BufferingApplicationStartup and FlightRecorderApplicationStartup behind a system property, and a /diag/startup endpoint that computes step self time -- the number /actuator/startup does not give you and the one that names the actual culprits. Captured under docs/output/: the step tree sorted both ways, the same startup as JFR events, a +5000-class experiment putting 0.11 ms per scanned class on the classpath scan tax, the silent truncation a 2048-step buffer performs, and JDK 25 AOT cache timings (6.93 s to 4.82 s). Post body and metadata live in post/. Moves the existing Actuator project into actuator-in-production/ so the repository holds one directory per article; the root README is now an index.
3.1 KiB
02 — Turning instrumentation on
← prev 01 — The number Boot logs · next → 03 — The four phases
There is no property for this
The single most common wasted hour: looking for spring.application.startup=buffering in
application.yaml. It does not exist. ApplicationStartup has to be set on the
SpringApplication before run(), because the steps you care about are recorded
before any configuration file has been read.
public static void main(String[] args) {
SpringApplication app = new SpringApplication(StartupDiagnosisApplication.class);
int capacity = Integer.getInteger("startup.buffer", 16384);
switch (System.getProperty("startup.tracking", "buffering")) {
case "buffering" -> app.setApplicationStartup(new BufferingApplicationStartup(capacity));
case "jfr" -> app.setApplicationStartup(new FlightRecorderApplicationStartup());
default -> { }
}
app.run(args);
}
That means changing which tracker you use is a redeploy, not a config change — worth
knowing before an incident, not during one. See
StartupDiagnosisApplication.java.
Exposing the endpoint
startup is not web-exposed by default:
management:
endpoints:
web:
exposure:
include: health,info,startup,beans,conditions
Exposing it without installing a tracker gives you an endpoint that returns an empty timeline rather than an error, which is a confusing way to lose twenty minutes.
GET peeks, POST drains
This is the trap that costs people their only recording. From
docs/output/02-startup-tree.txt:
GET /actuator/startup -> events in response: 400
POST /actuator/startup -> events in response: 400
POST /actuator/startup -> events in response: 0
GET /actuator/startup -> events in response: 0
POST calls drainBufferedTimeline(), which empties the buffer so the memory can be
reclaimed. Every guide shows the POST. If you pipe it to a file and the file is wrong, the
data is gone — the application has to be restarted to record it again. Use GET while you
are still working out what you want.
You do not need a @Bean for it
Boot registers the instance as a singleton named applicationStartup before the context
refreshes, so it can simply be injected. Declaring your own @Bean produces:
Parameter 0 of constructor in ...StartupDiagnosticsEndpoint required a single bean,
but 2 were found:
- bufferingApplicationStartup: defined by method 'bufferingApplicationStartup' ...
- applicationStartup: a programmatically registered singleton
Inject the ApplicationStartup interface and narrow with instanceof, not the concrete
BufferingApplicationStartup — otherwise the application refuses to start whenever
somebody runs it without tracking. Full transcript in
docs/output/01-api-corrections.txt.
← prev 01 — The number Boot logs · next → 03 — The four phases