CodeGym /Kurse /Spring Boot /Startup-Visibility in Spring Boot

Startup-Visibility in Spring Boot

Spring Boot
Level 23, Lektion 2
Verfügbar

1. Startup timeline statt einer einzelnen Zeile in den Logs

Manchmal scheint die Zeile in den Logs wie Started CatalogServiceApplication in 2.3 seconds bereits die Diagnose zu sein. Aber das ist eher die „Temperatur im Krankenhaus“: Es ist zwar klar, ob es „schnell/langsam“ ist, aber es ist nicht klar, was genau langsam war. Der Endpunkt /actuator/startup verwandelt den Start in eine Abfolge messbarer Schritte – und das verändert die Qualität des Gesprächs über das Problem erheblich.

Seien wir ehrlich: Die meisten Startprobleme beginnen nicht mit „alles kaputt“, sondern mit „gestern hat es in 1 Sekunde gestartet, heute in 8“. Und dann beginnt das Lieblingsspiel des Anfängers: „Ich habe nichts angefasst“. Spring Boot könnte in diesem Moment auch sagen: „Ich habe auch nichts angerührt“, denn meistens ist nicht Boot schuld, sondern Ihre Änderungen im Code, in der Konfiguration oder den Abhängigkeiten.

Startlogs bieten einen hervorragenden Gesamtüberblick, aber sie sind nicht verpflichtet zu erklären, wo genau die Zeit verbracht wurde. In einem realen Projekt können Sie auf schwere Abschnitte stoßen: teure Initialisierung eines Beans, Lesen großer Konfigurationen, komplexe Logik in @PostConstruct, ein „intelligenter“ ApplicationRunner, der die halbe Arbeit erledigt, bevor die Anwendung READY ist. Und genau hier ist es wichtig, den Start als Zeitachse zu sehen.

/actuator/startup hat eine einfache Superkraft: Es erlaubt, nicht nach „Gefühl“ zu streiten, sondern die Startschritte als Fakten zu betrachten. Es ist kein JVM-Profiler, kein Ersatz für Logs und keine „Pille für alle Krankheiten“, aber es ist genau das Werkzeug, das gut auf die Frage antwortet: „Wohin ist die Zeit beim Start geflossen, wenn es früher schneller war?“

2. ApplicationStartup als eingebaute Stoppuhr

Damit der Endpunkt startup überhaupt etwas anzeigen kann, muss Spring eine Datenquelle haben. Diese ist der Mechanismus ApplicationStartup. Wenn man es menschlich betrachtet, ist es ein in das Framework eingebautes „Messungsprotokoll“: Spring markiert an verschiedenen Startstellen Schritte, benennt sie, fügt manchmal Tags hinzu (z. B. den Bean-Namen), und am Ende entsteht eine Sammlung von Messungen, die analysiert werden kann.

Das Wichtigste hier ist der Gedanke, dass Spring nicht „alles auf der Welt“ misst, sondern konkrete Schritte, die es instrumentieren kann. Das Datenmodell sieht etwa so aus: Es gibt einen Schritt (StartupStep) mit einem Namen (z. B. etwas über die Erstellung eines Beans), der Schritt hat eine eindeutige id, manchmal eine parentId (um eine Hierarchie aufzubauen), und eine Reihe von Tags key/value, damit der Schritt nicht abstrakt, sondern an der Realität verankert ist (z. B. beanName=courseCatalogSeeder).

Für Anfänger ist das besonders nützlich, weil Sie nicht das gesamte Spring „auswendig“ kennen müssen. Selbst die grundlegende Fähigkeit zu sehen: „Hier ist ein Schritt, hier ist der Name, hier ist die Dauer, hier ist ein Tag mit dem Bean-Namen“ macht die Diagnose erheblich einfacher. Die Terminologie ist hier auch nicht beängstigend: ApplicationStartup ist ein Interface (Vertrag), und die konkrete Implementierung bestimmt, wohin diese Messungen abgelegt werden.

Der Actuator-Endpunkt /actuator/startup versucht genau diese Messungen nach außen zu zeigen. Aber es gibt einen Haken, und der ist grundlegend: Wenn Spring nichts gesammelt hat, gibt es nichts zu zeigen. Daher müssen wir eine solche Implementierung von ApplicationStartup aktivieren, die die Schritte im Speicher behält und für den Endpunkt zugänglich macht.

3. BufferingApplicationStartup: Puffer und Preis

BufferingApplicationStartup ist eine Implementierung von ApplicationStartup, die die gesammelten Startschritte im Speicher (im Grunde in einem Puffer) speichert. Genau diese wird benötigt, damit der Endpunkt /actuator/startup ein verständliches JSON zurückgeben kann. Die Logik ist einfach: Zuerst aktivieren wir die Datenerfassung (sonst gibt es nichts zu zeigen), dann stellt Actuator diese Daten über den Endpunkt bereit.

Es ist wichtig zu verstehen, dass das Wort „buffering“ hier nicht „Beschleunigung“ bedeutet, sondern „Ansammlung“. Es ist, als ob man eine Kamera aufnimmt: Man hat das Ereignis nicht beschleunigt, man kann es jetzt nur noch einmal ansehen. Aber die Kamera hat einen Preis – Speicher und geringe Gemeinkosten für die Datenerfassung. In den meisten Lern- und lokalen Szenarien ist das in Ordnung, aber eine solche Diagnose „überall und immer“ gedankenlos einzuschalten, ist nicht empfehlenswert.

BufferingApplicationStartup wird mit einem Größenparameter erstellt, z. B. new BufferingApplicationStartup(2048). Umgangssprachlich bedeutet das: „Wie viele Startschritte sind wir bereit zu speichern“. Wenn die Zahl zu klein gewählt wird, riskiert man, den Anfang der Zeitachse zu „beschneiden“ oder die Detailgenauigkeit zu verlieren. Wenn man eine sehr große Zahl wählt, speichert man einfach mehr Informationen, als man tatsächlich benötigt, und zahlt mit Speicher.

Für unseren catalog-service (einen kleinen Read-Only-Dienst) ist ein Wert wie 2048 eine komfortable und verständliche Option. Er bietet genügend Platz, um zu sehen, wie die Hauptteile der Anwendung hochgefahren werden: Kontext, Web-Infrastruktur, Konfiguration, Ihre Anwendungs-Beans. Gleichzeitig wird die Diagnose nicht zu einem separaten Subsystem, das wie ein Produkt verwaltet werden muss.

Ein weiterer wichtiger Punkt: Der Endpunkt /actuator/startup ist direkt darauf angewiesen, dass SpringApplication mit BufferingApplicationStartup vor dem Aufruf von run(...) konfiguriert wurde. Das heißt, später „einschalten“ geht nicht mehr: Wie bei einer Videoaufnahme eines Fußballspiels – wenn Sie die Aufnahme nach dem zweiten Tor eingeschaltet haben, sehen Sie das erste nicht, egal wie sehr Sie sich bemühen.

4. Aktivierung der Schritterfassung in main()

Das Schönste an diesem Thema ist, dass die Konfiguration buchstäblich an einer Stelle erfolgt – in main(). Ja, das ist der seltene Fall, in dem die „Spring-Magie“ tatsächlich mit einer einfachen Java-Zeile gesteuert wird. Wir erstellen ein Objekt SpringApplication, aktivieren BufferingApplicationStartup und starten dann erst die Anwendung. Vor run() – das ist wichtig.

In unserem Projekt catalog-service ist der Einstiegspunkt CatalogServiceApplication. Wenn Sie zuvor das klassische verwendet haben:

SpringApplication.run(CatalogServiceApplication.class, args);

dann müssen wir diesen Start jetzt „entfalten“, um die Konfiguration einzufügen:

package com.example.catalogservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;

@SpringBootApplication
public class CatalogServiceApplication {

    public static void main(String[] args) {
        // Erstelle SpringApplication manuell, um die Erfassung der Startschritte VOR run() zu aktivieren
        SpringApplication app = new SpringApplication(CatalogServiceApplication.class);

        // Aktiviere die Pufferung der Startschritte.
        // Wichtig: 2048 ist die Anzahl der Schritte, nicht Millisekunden und nicht „Startzeit“
        app.setApplicationStartup(new BufferingApplicationStartup(2048));

        // Starte die Anwendung nun mit aktivierter Erfassung der Startmetriken
        app.run(args);
    }
}

Hier sind drei Ideen wichtig.

Erste Idee: Wir ändern weder die Architektur der Anwendung noch die Beans. Wir sagen Boot nur: „Bitte sammle die Startmessungen“.

Zweite Idee: Die Puffergröße (2048) ist weder „Zeit“ noch „Millisekunden“, sondern die Anzahl der Schritte, die wir speichern möchten. Dies ist eine sehr häufige Verwechslung.

Dritte Idee: Wenn Sie zurück zu SpringApplication.run(...) wechseln, funktioniert der Endpunkt startup möglicherweise nicht mehr, nicht weil Sie „etwas im Actuator kaputt gemacht haben“, sondern weil Sie die Datenerfassung eingestellt haben. Das heißt, startup ist ein Diagnoseendpunkt, der davon abhängt, wie Sie die Anwendung starten.

Wenn DevTools aktiviert sind und Sie die Anwendung häufig neu starten, ist das ebenfalls in Ordnung. Jeder vollständige Start sammelt seinen eigenen Satz von Schritten neu, und /actuator/startup zeigt die Zeitachse des jeweils letzten Starts an. Das ist praktisch: Sie können Code/Konfiguration ändern und buchstäblich beobachten, wie sich das Startbild verändert.

5. Exposition von /actuator/startup in der Konfiguration

Selbst wenn Daten gesammelt werden, ist Actuator nicht verpflichtet, sie „einfach so“ nach außen zu zeigen. Standardmäßig ist Spring Boot sehr konservativ: Es werden nur wenige Endpunkte über HTTP exponiert (normalerweise health), der Rest muss explizit über management.endpoints.web.exposure.include aktiviert werden. Und das ist eine gute Gewohnheit: Die Diagnose sollte kontrolliert und nicht zufällig sein.

Wenn die Startschritte bereits gesammelt werden, muss der Endpunkt nur im selben local-Profil geöffnet werden, in dem sich auch die übrige Diagnose befindet. Auch hier ist die kumulative Logik wichtig: Wir fügen startup zur bereits vorhandenen Liste hinzu, anstatt include speziell für diese Lektion neu zu schreiben.

Minimalbeispiel:

management:
  endpoints:
    web:
      exposure:
        # Bereits geöffnete local/dev-Endpunkte beibehalten und startup hinzufügen
        include: "health,info,env,configprops,conditions,mappings,startup"

Danach ist der Endpunkt unter folgendem Pfad erreichbar:

GET /actuator/startup

Wenn Sie 404 Not Found sehen, schimpfen Sie nicht gleich auf Actuator. In diesem Zusammenhang bedeutet 404 meistens eines von zwei Dingen: entweder ist der Endpunkt nicht exponiert (nicht in include enthalten) oder der Endpunkt wurde gar nicht erstellt, weil die Anwendung keine Startschritte sammelt (kein BufferingApplicationStartup in main()).

Ein weiterer subtiler Punkt: In den JSON-Antworten von Actuator werden manchmal spezifische Content-Type verwendet (wie application/vnd.spring-boot.actuator.v3+json). Als Entwickler des catalog-service müssen Sie sich damit normalerweise nicht manuell herumschlagen – Browser und curl „verdauen“ das. Aber wenn Sie den Endpunkt über einen Client testen, der empfindlich ist, denken Sie daran: Actuator ist eine API mit eigener Semantik und Formaten.

6. Analyse der JSON-Antwort von /actuator/startup

Die Antwort von /actuator/startup kann leicht erschrecken: viele Zeiten, Bezeichner, verschachtelte Objekte und seltsame Dinge wie PT0.00018S. Tatsächlich ist die Struktur recht regelmäßig. Liest man es als Zeitachse von Ereignissen und nicht als „zufälliges JSON“, wird schnell alles klar: Es gibt eine Startzeit, eine Liste gemessener Ereignisse, jedes hat eine Dauer und eine Beschreibung des Schrittes.

Ein vereinfachter Antwortausschnitt könnte so aussehen (stark gekürzt, damit das Auge nicht überläuft):

{
  "springBootVersion": "4.0.3",
  "timeline": {
    "startTime": "2026-03-19T12:00:00.000Z",
    "events": [
      {
        "duration": "PT0.150S",
        "startupStep": {
          "name": "spring.boot.application.starting",
          "id": 0,
          "tags": [
            { "key": "mainApplicationClass", "value": "com.example.catalogservice.CatalogServiceApplication" }
          ]
        }
      },
      {
        "duration": "PT0.012S",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 12,
          "parentId": 5,
          "tags": [
            { "key": "beanName", "value": "courseCatalogController" }
          ]
        }
      }
    ]
  }
}

Um nicht zu versuchen, „alles auf einmal“ zu merken, ist es hilfreich, eine kleine Gedächtnisstütze zu haben, was hier was bedeutet:

Feld Was es in menschlichen Worten bedeutet
timeline.startTime
Moment, in dem der messbare Start begann
timeline.events[]
Liste der gemessenen Ereignisse (Schritte)
events[].duration
Dauer im ISO-8601-Duration-Format (
PT0.150S
= 0,150 Sekunden)
events[].startupStep.name
Name des Schrittes (Operationstyp), beginnt oft mit spring.*
events[].startupStep.id
ID des Schrittes, um Ereignisse voneinander zu unterscheiden
events[].startupStep.parentId
Übergeordneter Schritt (falls Hierarchie vorhanden)
events[].startupStep.tags[]
Nützliche „Markierungen“, z. B. Bean-Name (beanName)

Besondere Aufmerksamkeit verdient das Format der Dauer PT.... Es sieht aus wie ein Zauberspruch, ist aber nur der ISO-8601-Standard für Dauern. PT0.150S sind 150 Millisekunden. Wenn es z. B. PT2.345S ist, sind das 2,345 Sekunden. Ja, das Format ist etwas seltsam, aber dafür eindeutig und von Werkzeugen leicht lesbar.

Und noch ein wichtiger psychologischer Punkt: Versuchen Sie nicht, jeden einzelnen Schritt zu analysieren. Sie müssen fast nie „alles lesen“. Normalerweise suchen Sie nach Anomalien: Schritte, die deutlich mehr Zeit in Anspruch nehmen als andere, oder sich wiederholende schwere Stellen.

7. Praxis: Suche nach langsamen Stellen beim Start

Der wichtigste praktische Nutzen der Startup-Zeitachse ist nicht, „die Anwendung um jeden Preis zu beschleunigen“, sondern das Verhalten zu erklären und die konkrete Ursache zu finden, wenn der Start unerwartet langsam geworden ist. Das ist ein wichtiger Unterschied: Die Diagnose dient dem Denken, nicht der Panik.

Ein sehr typisches Szenario: Sie haben ein neues Bean zum Projekt hinzugefügt (z. B. eine Komponente im Paket catalog.bootstrap), und plötzlich ist der Start gewachsen. Mit /actuator/startup können Sie die Schritte spring.beans.instantiate mit dem Tag beanName=... sehen. Wenn einer dieser Schritte plötzlich, sagen wir, 1–2 Sekunden dauert, haben Sie einen konkreten Ansatzpunkt: „Hier ist das konkrete Bean, das schwer zu erstellen ist“. Dann durchsuchen Sie nicht die gesamte Anwendung, sondern gehen direkt zu dieser Klasse und schauen, was dort passiert.

Im Lern-catalog-service ist dies besonders bei solchen Komponenten aufschlussreich, die logischerweise beim Start Arbeit verrichten. Wenn Sie z. B. einen Datenseeder haben, der Kurse aus der Konfiguration lädt, und Sie (versehentlich oder absichtlich) diesen Ladevorgang in @PostConstruct schwer gemacht haben, dann wird das Bean den Start „belasten“. Hier ist ein Beispiel für die Struktur einer solchen Komponente (ohne überflüssige Logik – uns interessiert hier der Lebenszyklus):

package com.example.catalogservice.catalog.bootstrap;

import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
class CourseCatalogSeeder {

    private static final Logger log = LoggerFactory.getLogger(CourseCatalogSeeder.class);

    @PostConstruct
    void seed() {
        log.info("Seeding catalog data...");
    }
}

@PostConstruct wird zum Zeitpunkt der Bean-Initialisierung ausgeführt, und wenn darin etwas Schweres steckt, kann es sich durchaus auf die Startzeitachse auswirken. Und dann hilft /actuator/startup zu sehen: „Aha, die Erstellung/Initialisierung dieses Beans nimmt merkliche Zeit in Anspruch“.

Der zweite nützliche Ansatz ist, nicht auf absolute Zahlen zu schauen, sondern auf Vergleiche. Wenn der Start gestern 1,2 Sekunden dauerte und heute 3,8, und Sie sehen, dass ein neuer „langer“ Schritt aufgetaucht ist, ist das ein sehr starkes Signal. Wenn der Start jedoch von 1,2 auf 1,4 gestiegen ist und Sie mehrere Schritte sehen, die etwas länger dauern, könnte es sich um Rauschen handeln: Die Maschine war ausgelastet, der JIT hat sich anders aufgewärmt, und das Leben ist generell komplizierter als perfekt gleichmäßige Millisekunden.

Dritter Punkt – die Grenzen der Anwendbarkeit. Der Startup-Endpunkt zeigt das, was während des Starts der Anwendung passiert ist, nicht zur normalen Laufzeit. Wenn Sie das Problem haben, dass „die ersten Anfragen nach dem Start langsam antworten“, kann /actuator/startup zwar das „Gesamtbild“ liefern, aber es ist kein Werkzeug zur Analyse der Anfrageleistung. Es geht um den Start, und das ist seine Stärke.

Und schließlich ein angenehmer Bonus: Der Startup-Endpunkt hilft bei Diskussionen im Team. Statt „ich habe das Gefühl, es ist langsamer geworden“ kommt „hier ist der Schritt spring.beans.instantiate für courseCatalogSeeder, Dauer soundso“. Das verwandelt die Diskussion fast magisch in eine technische.

Snapshot und Drain

Manchmal ist es nützlich zu wissen, dass /actuator/startup in zwei Modi arbeiten kann: einen „Snapshot“ der Daten abrufen oder die Daten aus dem Puffer „abfließen lassen“ (Drain). In den meisten Fällen reicht das normale Lesen (Snapshot) aus, aber die Tatsache, dass es „Drain“ gibt, hilft zu verstehen, dass die Daten wirklich im Puffer leben und wie eine Ressource behandelt werden können.

Im typischen Actuator-API-Modell sieht das so aus: GET gibt einen Snapshot zurück, POST gibt die Daten zurück und leert den Puffer. Der Sinn dahinter ist einfach: Wenn Sie aus irgendeinem Grund Speicher freigeben möchten oder nicht möchten, dass die Daten weiter gespeichert werden, können Sie sie einmal „ableiten“ und danach ist der Endpunkt leer, bis es einen neuen Start gibt.

Für den Kurs reicht eine kurze Regel: Wenn Sie einfach sehen möchten, wie die Anwendung gestartet ist, verwenden Sie GET /actuator/startup. Wenn Sie die Daten abholen und den Puffer leeren möchten (z. B. für eine einmalige Diagnose), verwenden Sie POST /actuator/startup. In den meisten Lernscenarien ist das nicht kritisch, aber das Verständnis der Mechanik macht Actuator weniger „magisch“.

8. Typische Fehler

Fehler Nr. 1: startup „für alle Fälle“ überall exponieren, auch in der Produktion.
Startup-Informationen sind Diagnose und können in einer öffentlichen Umgebung überflüssig sein. In unserem Lernprojekt halten wir uns an eine sichere Politik: Erweiterte Endpunkte leben in local/dev und werden nicht versehentlich zu einer öffentlichen Angriffsfläche. Auch wenn es scheint, dass „da nichts Geheimes ist“, ist es dennoch eine Vergrößerung der Angriffsfläche und zusätzliche Informationen über die Interna der Anwendung.

Fehler Nr. 2: Endpunkt in exposure.include aufnehmen, aber BufferingApplicationStartup vergessen.
Dies ist der häufigste Grund für 404 oder „Leere“: Sie haben die Route geöffnet, aber es gibt keine Daten, weil Sie sie nicht sammeln. Der Endpunkt startup ist nicht „autark“: Er benötigt eine Messquelle, und diese Quelle wird in main() vor run() aktiviert.

Fehler Nr. 3: Einen winzigen Puffer einstellen und sich dann wundern, dass die „Zeitachse irgendwie abgeschnitten“ ist.
Wenn Sie eine kleine Anzahl von Schritten angeben, speichert Spring weniger Messungen. Bei kleinen Anwendungen kann das unbemerkt bleiben, aber sobald das Projekt wächst, kann die Detailgenauigkeit verloren gehen. Für catalog-service ist ein vernünftiger Wert wie 2048 eine gute Balance zwischen „genug Daten“ und „wir machen aus der Diagnose kein separates Produkt“.

Fehler Nr. 4: Versuchen, „alles auf einmal“ nach einer einzelnen Zahl zu optimieren.
Die Startup-Zeitachse ist keine Liste der „Schuldigen“, sondern eine Karte. Einen Schritt von 200 ms zu sehen und sofort die Architektur umzuschreiben, ist typische vorzeitige Optimierung. Viel gesünder ist es, den Endpunkt als Richtungsweiser zu nutzen: Wo ist die Ursache zu suchen, wenn der Start tatsächlich zum Problem wird?

Fehler Nr. 5: Erwarten, dass /actuator/startup Probleme der normalen Laufzeit nach dem Start erklärt.
Wenn die Anwendung in einer Sekunde startet, aber die ersten Anfragen dann „träge“ sind, hat der Startup-Endpunkt damit möglicherweise gar nichts zu tun. Er misst die Startsequenz, nicht die Leistung der Anfrageverarbeitung. Das ist normal: Jedes Werkzeug hat seinen eigenen Verantwortungsbereich.

1
Aufgabe
Spring Boot, Level 23, Lektion 2
Gesperrt
Endpoint `startup` mit `BufferingApplicationStartup`
Endpoint `startup` mit `BufferingApplicationStartup`
1
Aufgabe
Spring Boot, Level 23, Lektion 2
Gesperrt
Benannter Startup-Komponente in der Timeline
Benannter Startup-Komponente in der Timeline
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION