CodeGym /Kurse /Spring Core /Startzyklus des ApplicationContext

Startzyklus des ApplicationContext

Spring Core
Level 19, Lektion 0
Verfügbar

1. Bedeutung des Verständnisses des Container-Starts

Wenn Sie zum ersten Mal einen Spring-Kontext starten, neigt man dazu, ihn wie eine Mikrowelle zu behandeln: Knopf gedrückt – Essen fertig. Aber in Wirklichkeit ist ApplicationContext eher eine Restaurantküche: Zuerst wird die Bestellung aufgenommen, dann werden die Zutaten vorbereitet, die Geräte geprüft, erst dann wird das Gericht serviert. Wenn Sie diese Phasen nicht sehen, wirkt jeder Fehler beim Start wie „Spring schimpft wieder auf das ganze Universum“, und Sie behandeln das Problem durch Raten, nicht durch Verständnis.

Wir haben bereits gesehen, dass der Container Lifecycle-Callbacks aufrufen und Application Events liefern kann. Aber bisher kann man das leicht als fertigen Knopf betrachten: @PostConstruct hat funktioniert, @EventListener hat das Ereignis abgefangen, und gut. Damit solche Dinge nicht wie Zauberei wirken, muss man verstehen, wo innerhalb des refresh() diese Infrastruktur überhaupt angebunden wird und warum sie nur für Container-verwaltete Objekte funktioniert.

Das Nützlichste, was das mentale Modell des Starts bietet, ist die Fähigkeit zu erklären, warum die Anwendung abgestürzt ist, bevor der erste Aufruf von ScenarioRunner erfolgte, wann Singleton-Beans erstellt werden und wo im Prozess die „dienstlichen Verbesserer“ auftauchen, die @Autowired, @PostConstruct, @EventListener und viele andere Annehmlichkeiten ermöglichen. In dieser Vorlesung zeichnen wir eine große Karte des refresh()-Zyklus, aber ohne feine Details – Details kommen in den nächsten Vorlesungen des Tages.

Container-Start als „zwei Realitäten“: Metadaten und Objekte

Es ist wichtig, vorab einen einfachen Gedanken festzuhalten: Spring hat zwei „Welten“. In der ersten Welt operiert der Container mit Beschreibungen von Beans (BeanDefinition) – das sind noch keine Objekte, sondern Rezepte und Regeln. In der zweiten Welt arbeitet der Container mit realen Instanzen, die im JVM-Speicher erstellt wurden. Der Start des Containers ist genau der Übergang von der Welt der „Beschreibungen“ zur Welt der „fertigen Objekte“, und er geschieht nicht augenblicklich.

2. Zweischrittiger Start: register() und refresh()

Wahrscheinlich sind Sie an die Kurzform des Kontextstarts gewöhnt: new AnnotationConfigApplicationContext(AppConfig...). Sie ist bequem, verbirgt aber ein wichtiges pädagogisches Detail: Intern macht Spring ungefähr die gleichen Schritte, die wir manuell machen würden. Um die Grenze zwischen „Kontext hat Konfiguration erhalten“ und „Kontext ist wirklich gestartet“ zu sehen, ist es manchmal nützlich, ihn in zwei Schritten zu starten: register() und refresh().

Das ist kein „spezieller Modus“, keine seltene Funktion und kein „Hack aus Härte“. Es ist einfach eine Möglichkeit, anschaulich zu sehen: Vor refresh() haben wir noch keinen vollwertigen Container, der bereit ist, die Anwendung zu bedienen, und nach refresh() ist er da. In ContextFlow hilft das zu verstehen, warum manche Fehler auftreten, bevor Sie überhaupt ScenarioRunner erhalten haben, und warum der Container zuerst ein Bild aufbaut und erst dann beginnt, eager Singleton-Beans zu erstellen.

Minibeispiel: explizites register() → refresh() → close()


import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApp {
    public static void main(String[] args) {
        // Erstellen Sie den Kontext "leer": Er enthält noch nicht Ihre Beans und hat den Refresh-Pipeline noch nicht durchlaufen
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        // Registrieren Sie die Konfiguration: In dieser Phase erhält Spring nur die "Eingabedaten"
        context.register(AppConfig.class);

        // Starten Sie die vollständige Startup-Pipeline: Hier wird die Infrastruktur erscheinen und eager Singleton-Beans beginnen erstellt zu werden
        context.refresh();

        // Nach refresh() können Sie sicher Beans abrufen und Szenarien starten
        ScenarioRunner runner = context.getBean(ScenarioRunner.class);
        runner.run();

        // Schließen Sie den Kontext korrekt: Die Destroy-Phase wird gestartet und Destroy-Callbacks werden aufgerufen
        context.close();
    }
}

Hier ist es wichtig, die Zeilen wie eine Geschichte zu lesen. Nach register() haben wir nur gesagt: „Container, hier ist deine Konfiguration“. Aber refresh() ist der eigentliche Start, bei dem Spring die Konfiguration liest, die Infrastruktur vorbereitet, die benötigten Singleton-Beans erstellt und erst dann den Kontext als „bereit“ erklärt. close() beendet die Arbeit symmetrisch und gibt dem Container die Chance, Destroy-Callbacks korrekt auszuführen.

In einem echten Projekt werden Sie oft try-with-resources verwenden, um nicht zu vergessen, den Kontext zu schließen (der Kontext ist wie Kaffee: besser rechtzeitig schließen, sonst wird er kalt und seltsam).

3. BeanDefinition und fertiger Bean: Rezept und Gericht

Als wir in früheren Modulen über BeanDefinition gesprochen haben, klang das vielleicht wie „na ja, Metadaten, okay“. Heute wird dieser Unterschied praktisch, denn in refresh() arbeitet der Container zunächst fast ausschließlich in der „Rezeptwelt“. Und erst später, wenn er die Vorbereitung abgeschlossen hat, beginnt er massenhaft reale Instanzen zu erstellen.

Stellen Sie sich vor, BeanDefinition ist eine Rezeptkarte: welche Klasse zu erstellen, welcher Scope, welche Abhängigkeiten benötigt werden, welche Callbacks vorhanden sind. Ein Bean-Instance ist das fertige Gericht auf dem Teller. Wenn Sie versuchen, das „Rezept zu essen“, bekommen Sie keine Kalorien. Wenn Sie in der Rezeptphase Methoden des fertigen Objekts aufrufen, existiert das Objekt noch nicht, und Sie erhalten einen Fehler oder seltsames Verhalten.

Minitablelle: Was zu verschiedenen Startzeitpunkten existiert

Zeitpunkt Im Container bereits vorhanden Im Container noch nicht vorhanden Wie sich das für einen Anfänger anfühlt
Vor refresh() einige grundlegende BeanDefinition (einschließlich für die Konfiguration) die meisten Definitions aus Scanning und @Bean, und erst recht keine realen Singleton-Instanzen getBean(...) funktioniert normalerweise nicht, der Kontext ist noch „nicht hochgefahren“
Während refresh() (frühe Phasen) die Menge der BeanDefinition wird aktiv aufgebaut und verändert normale Business-Instanzen sind noch nicht erstellt Fehler sehen oft aus wie „Startup-Failure“ vor der Ausführung Ihres Codes
Ende von refresh() Definitions sind bereit, Infrastruktur ist bereit es bleiben nur @Lazy (sie müssen möglicherweise nicht erstellt werden) ScenarioRunner kann abgerufen und Szenarien können ausgeführt werden

Diese Tabelle handelt nicht von „genauen internen Methoden von Spring“, sondern von einer praktischen Orientierung: Wenn Sie etwas „zu früh“ versuchen, stoßen Sie auf eine Phasengrenze.

4. Karte von refresh(): Startup-Pipeline

Wenn Sie den Spring-Quellcode öffnen und anfangen, AbstractApplicationContext#refresh zu lesen, können Sie versehentlich drei Stunden später aufwachen mit der Frage „Warum habe ich diesen Beruf überhaupt gewählt?“. Deshalb machen wir eine ehrliche, aber freundliche Ebene: kein „Source-Code Deep Dive“, sondern ein technisches Schema, bei dem jede Stufe die Frage beantwortet „Was macht der Container gerade und warum“.

Die Bedeutung von refresh() kann man so beschreiben: Der Container sammelt und ordnet zunächst die Metadaten, dann bindet die Infrastruktur an, dann erstellt er eager Singleton-Beans und dann erklärt er sich für bereit und veröffentlicht das Ereignis „Kontext hochgefahren“. Und genau innerhalb dieser Stufen befinden sich die Erweiterungspunkte (Post-Processing), die wir heute später untersuchen werden.

Mermaid-Schema: von der Konfiguration zum fertigen Kontext

flowchart TD
    %% Idee: Grenze zwischen „Beschreibungen“ (Metadaten) und „Objekte“ (Instanzen) zeigen und wo refresh() passiert
    %% Post-Processing ist eine Kategorie von Schritten in der Mitte, kein einzelner Aufruf
    A["register(AppConfig)"] --> B["refresh() Start"]
    B --> C["Lesen der Konfiguration und Registrierung von BeanDefinition"]
    C --> D["Dienstverarbeitung von Metadaten (Post-Processing)"]
    D --> E["Anbindung der Container-Infrastruktur"]
    E --> F["Erstellung von eager Singleton-Beans"]
    F --> G["Kontext bereit: getBean und Szenarien starten"]
    G --> H["close() / Shutdown: Destroy-Phase"]

Beachten Sie: „Post-Processing“ im Schema ist nicht „eine Methode“. Es ist eine ganze Kategorie von Aktionen. Heute platzieren wir es nur auf der Karte. In den Vorlesungen 2 und 3 werden wir bereits die zwei Hauptgruppen dieser Erweiterungen besprechen: Eine arbeitet mit BeanDefinition, die andere mit fertigen Instanzen.

Etwas konkreter, aber ohne Überlastung mit internen Methodennamen

Um eine gemeinsame Sprache zu haben, halten wir die Sequenz fest (in Erzählform, ohne sie in eine „Liste mit 20 Punkten“ zu verwandeln). refresh() beginnt mit der Vorbereitung des Kontexts: Der Container prüft die grundlegenden Startbedingungen, bereitet interne Strukturen vor. Dann holt/aktualisiert er seine BeanFactory (den internen „Motor“ des Containers). Dann lädt und registriert er BeanDefinition für Komponenten, Konfigurationen, @Bean-Methoden usw.

Danach beginnt der „interessanteste Teil“: Der Container startet die Dienstverarbeitung, die Metadaten ändern und die Infrastruktur anbinden kann. Danach registriert er Handler, die bei der Erstellung jeder Bean teilnehmen (einschließlich rund um die Init-Phase). Und schon gegen Ende erstellt er alle nicht-lazy Singleton-Beans und führt für jede den Ihnen bekannten Lifecycle aus: Konstruktor, DI, Init-Callbacks. Erst danach gilt der Kontext als hochgefahren.

5. Bean-Lifecycle innerhalb des Kontexts

Nach den Vorlesungen über den Bean-Lifecycle haben viele Studenten das Gefühl: „Ich kenne den Lebenszyklus: constructor → @PostConstruct → @PreDestroy. Also weiß ich alles über den Start.“ Aber in Wirklichkeit ist der Lebenszyklus einer einzelnen Bean wie das Leben eines einzelnen Mitarbeiters in einem Unternehmen. Nützlich, aber das Unternehmen durchläuft noch Einstellung, Onboarding, Laptop-Kauf, VPN-Einrichtung und Firmenveranstaltungen. Der Lifecycle des Kontexts ist weiter, und der Bean-Lifecycle ist nur ein Fragment darin.

Wichtige Erkenntnis: Wenn Sie System.out.println im Konstruktor einer Bean sehen, ist das nicht „etwas wurde aufgerufen“, sondern ein Zeichen dafür, dass der Container zur Stufe der Erstellung von Singletons gelangt ist. Und wenn der Konstruktor nicht aufgerufen wurde, ist das nicht unbedingt ein Fehler: Vielleicht ist die Bean @Lazy, oder sie ist ein Prototype, oder sie wird für den Eager-Start nicht benötigt. Daher müssen wir in der Lage sein, das beobachtbare Verhalten einer Bean mit der Phase des refresh() zu verknüpfen.

Minibeispiel: Eine Bean, die zeigt „ich wurde erstellt und initialisiert“


import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
class StartupProbe {

    public StartupProbe() {
        // Der Konstruktor wird aufgerufen, wenn der Container zur Erstellung von eager Singletons gelangt
        System.out.println("StartupProbe: constructor"); // StartupProbe: constructor
    }

    @PostConstruct
    void init() {
        // @PostConstruct wird nach DI und bevor die Bean von anderen Beans/Szenarien genutzt wird, ausgeführt
        System.out.println("StartupProbe: @PostConstruct"); // StartupProbe: @PostConstruct
    }
}

Dieses Beispiel ist bewusst einfach und etwas „laut“ – es dient als Sensor. Wenn Sie diese Zeilen bei context.refresh() sehen, bedeutet das, dass der Container zur Phase der Erstellung von eager Singletons gelangt ist und die Init-Logik aufgerufen hat. Wenn Sie sie nicht sehen, bedeutet das entweder, dass die Bean nicht in den Kontext gelangt ist, oder sie ist lazy, oder der Kontext ist früher abgestürzt.

Und ja, einen solchen Sensor muss man nicht für immer im Projekt behalten. Es ist wie eine Testlampe auf einer Platine: Beim Debuggen nützlich, im finalen Build normalerweise nicht benötigt.

6. Eager-Singleton-Start und @Lazy

Viele Anfänger verwechseln zwei Aussagen: „Bean ist registriert“ und „Objekt ist erstellt“. In Spring sind das verschiedene Dinge, und @Lazy ist der beste Weg, das selbst zu erfahren. @Lazy hat nicht die Aufgabe, „die Welt zu beschleunigen“. Seine Aufgabe ist es, die Erstellung eines bestimmten Objekts bis zur ersten tatsächlichen Anfrage zu verschieben. Aber die BeanDefinition dieser Bean existiert dennoch und nimmt am Wiring und der Diagnose teil.

Das ist besonders wichtig für das Verständnis der Startup-Pipeline: Der Großteil der Infrastruktur und der eager Singletons wird beim Start erstellt, während lazy Beans erst während der Ausführung von Szenarien erstellt werden können. Daher können „Fehler beim Start“ und „Fehler beim ersten Aufruf“ unterschiedlicher Natur sein: Einer tritt bei refresh() auf, der andere bei getBean() (oder bei der ersten Verwendung über eine Abhängigkeit).

Minibeispiel: Lazy Bean und ihr Erstellungszeitpunkt


import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
@Lazy
class LazyProbe {

    public LazyProbe() {
        // Dieser Konstruktor wird beim Start (refresh) NICHT aufgerufen, solange niemand explizit die Bean anfordert
        System.out.println("LazyProbe created"); // LazyProbe created
    }
}

Und jetzt eine kleine Provokation: Wenn Sie einfach den Kontext hochfahren, wird LazyProbe wahrscheinlich nicht erstellt. Aber wenn Sie sie explizit anfordern:


import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class LazyDemo {
    public static void main(String[] args) {
        // try-with-resources stellt sicher, dass der Kontext geschlossen wird und die Destroy-Phase ausgeführt wird
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Die erste tatsächliche Anfrage an eine lazy Bean provoziert die Erstellung der Instanz
            context.getBean(LazyProbe.class); // hier wird LazyProbe erstellt
        }
    }
}

Innerhalb von ContextFlow werden Sie häufiger damit konfrontiert, dass die meisten Services und Listener eager Singletons sind (Fail-Fast-Stil), und @Lazy nur punktuell eingesetzt wird, weil es die Art der Diagnose verändert: Ein Teil der Probleme wandert von der „Startzeit“ in die „Laufzeit“.

7. Ingenieurslupe für den Start in ContextFlow

Wenn man die Startup-Pipeline erklärt, neigt man leicht dazu, „lasst uns hundert Zeilen in die Konsole drucken und die Reihenfolge erraten“. Das werden wir nicht tun. Unser Ziel ist eine saubere Beobachtbarkeit: ein paar kleine Signale, die klar zeigen, was passiert, und dabei das Projekt nicht von Debug-Lärm abhängig machen.

Der praktischste Weg ist, ein oder zwei „Probe“-Komponenten zu haben (wie StartupProbe) und ein Diagnose-Code-Stück in main(), das den Unterschied zwischen „Kontext noch nicht refresht“ und „Kontext bereits refresht“ zeigt. Das ist genau das, was einem Anfänger hilft, den Container nicht mehr als Blackbox zu betrachten.

Minibeispiel: Zeigen, dass register() noch nicht „Kontext hochgefahren“ ist


import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class RefreshBoundaryDemo {
    public static void main(String[] args) {
        // Erstellen Sie den Kontext ohne Konfiguration: Hier gibt es fast keine Definitions
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        // Vor register(): Wir haben dem Container noch nicht einmal die "Rezepte" unserer Beans übergeben
        System.out.println("Before register: " + context.getBeanDefinitionCount()); // Before register: 0

        // Nach register(): Es taucht mindestens die Konfigurationsklasse als BeanDefinition auf
        context.register(AppConfig.class);
        System.out.println("After register: " + context.getBeanDefinitionCount());  // After register: 1

        // Nach refresh(): Spring fügt Infrastruktur und Definitionen für gefundene Komponenten/@Bean usw. hinzu
        context.refresh();
        System.out.println("After refresh: " + context.getBeanDefinitionCount());   // After refresh: (viel größer)

        // Schließen des Kontexts: korrekter Abschluss des Lebenszyklus
        context.close();
    }
}

Ja, die genauen Zahlen hängen davon ab, welche Module und Infrastruktur-Beans in Ihrem Projekt vorhanden sind, und das ist normal. Hier geht es nicht um Arithmetik, sondern um die Form: Nach register() sehen Sie „fast nichts“, nach refresh() wird der Kontext plötzlich „reich“.

In ContextFlow passt das sehr gut dazu, dass Sie bereits eine modulare Konfiguration (@Import), MessageSource, @EventListener-Listener usw. haben. Alle werden erst nach einem vollständigen Refresh-Zyklus zur „Realität“.

Wo ist der Platz für die „dienstliche Magie“

An diesem Punkt könnte bei Ihnen eine sehr gesunde Frage aufkommen: „Okay, der Container hat Beans erstellt. Wo genau macht er das, was @Autowired das Injizieren von Abhängigkeiten und @EventListener das Hören von Ereignissen ermöglicht?“ Die Antwort liegt in der Mitte von refresh(), wo es Phasen gibt, in denen die Infrastruktur angebunden und Post-Processing durchgeführt wird. Das ist wie das Stimmen der Musikinstrumente vor dem Konzert: Solange sie nicht gestimmt sind, beginnt das Konzert nicht, selbst wenn die Musiker schon da sind.

In dieser Vorlesung gehen wir bewusst nicht in die Tiefe von Klassen und Interfaces, denn das sind die nächsten Vorlesungen des Tages. Aber es ist wichtig festzuhalten: Diese Magie ist nicht „in Java eingebaut“ und funktioniert nicht „von selbst“. Sie lebt in den Container-Mechanismen des Starts. Und genau deshalb erhält ein Spring-Objekt, das mit new erstellt wurde, weder Autowiring noch @PostConstruct noch die Registrierung von Event-Listenern: Es hat die Pipeline nicht durchlaufen.

Um es im Kopf sauber zu haben, kann man sich diese Formel merken: „refresh() ist nicht nur die Erstellung von Objekten; es ist die Erstellung von Objekten plus die Anbindung der Infrastruktur, die diese Objekte verbessern und bedienen kann“.

8. Typische Fehler beim Start des Kontexts

Fehler Nr. 1: Annehmen, dass der Kontext im Moment von register() „hochfährt“.
register() ist lediglich die Registrierung von Konfigurationsklassen als Ausgangsdaten. Der eigentliche Start beginnt in refresh(). Wenn Sie diese Schritte verwechseln, werden Sie sich wundern, warum der Container „noch nichts weiß“ und warum getBean() nicht funktioniert, wenn Sie es erwarten.

Fehler Nr. 2: Verwechslung von BeanDefinition und Bean-Instanz.
Ein sehr häufiger Gedanke: „Wenn es eine Bean gibt, dann ist das Objekt bereits erstellt.“ Nein. Zu Beginn arbeitet der Container fast die ganze Zeit mit Metadaten, und die Erstellung von eager Singletons ist eine separate späte Phase. Wenn wir heute später über Container-Erweiterungen sprechen, wird dieser Unterschied entscheidend sein: Manche Mechanismen greifen die Definitionen an, andere die realen Objekte.

Fehler Nr. 3: Den Lifecycle einer einzelnen Bean als vollständige Beschreibung des Anwendungsstarts zu betrachten.
Der Lifecycle einer Bean ist wichtig, aber er ist „in den größeren Lifecycle des Kontexts eingebettet“. Wenn Sie @PostConstruct sehen, bedeutet das, dass der Container zu einer bestimmten Stufe gelangt ist – aber vorher gab es viele Schritte, und danach gibt es ebenfalls Schritte, einschließlich der Finalisierung des Starts und der Veröffentlichung von Kontext-Ereignissen.

Fehler Nr. 4: Denken, dass @Lazy die Bean „aus dem Container ausschaltet“.
@Lazy hebt die Registrierung der Bean nicht auf. Es ändert den Zeitpunkt der Instanzerstellung. Die Definition (BeanDefinition) bleibt, das Wiring nach Typen bleibt, aber das Objekt erscheint später. Das ändert oft die Diagnose: Fehler können vom Zeitpunkt des Starts auf den Zeitpunkt der ersten Nutzung verschoben werden.

Fehler Nr. 5: Die Ursache eines Startup-Failures im Business-Code zu suchen, ohne die Phase des Absturzes zu ermitteln.
Wenn der Kontext in refresh() abstürzt, liegt die Ursache sehr oft im Wiring/Konfiguration/Infrastruktur, nicht darin, dass „im OrderPlacementService die falsche Rabattformel steht“. Die Gewohnheit, sich zuerst zu fragen „In welcher Startphase sind wir abgestürzt?“ spart viel Zeit und Nerven – besonders wenn der Stacktrace wie ein Roman von Tolstoi aussieht.

1
Aufgabe
Spring Core, Level 19, Lektion 0
Gesperrt
Trennung von `register()` und `refresh()`
Trennung von `register()` und `refresh()`
1
Aufgabe
Spring Core, Level 19, Lektion 0
Gesperrt
Eager- und Lazy-Bean im selben Kontext
Eager- und Lazy-Bean im selben Kontext
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION