1. Platzierung des Startcodes
Jeder Entwickler stößt früher oder später auf den Wunsch, eine „kleine Sache beim Start“ zu erledigen. Zum Beispiel eine kurze Zusammenfassung auszugeben, zu überprüfen, ob die Anwendung im richtigen Modus gestartet ist, oder einen Cache im Speicher vorzubereiten. In Konsolen-Java-Anwendungen neigt man dazu, dies direkt in main() zu schreiben. In Spring Boot führt dies schnell zu einem unbequemen und schlecht wartbaren Stil: main() wird zur Müllhalde, und die Startlogik beginnt mit der Art und Weise zu kollidieren, wie Boot den Lebenszyklus und die Abhängigkeiten verwaltet.
Seien wir ehrlich: main() in einem Boot-Projekt ist eher ein „Einschaltknopf“ als ein „Ort, an dem die Geschäftslogik lebt“. Wenn Sie anfangen, echten Startcode in main() zu schreiben, bekommen Sie sehr schnell drei typische Probleme.
Das erste Problem ist, dass der Code in main() fast immer beginnt, Abhängigkeiten manuell zu erstellen. Entweder Sie schreiben new (und verlieren DI) oder Sie versuchen, einen Bean aus dem Kontext zu holen (und das riecht schon seltsam). Das zweite Problem ist, dass main() schwer zu testen und zu lesen ist: es sollte einfach sein, sonst wird jeder neue Entwickler Ihren Start wie einen Krimi lesen. Das dritte Problem ist, dass main() vor dem ordnungsgemäßen Aufbau der Anwendung ausgeführt wird: viele Dinge (insbesondere solche, die Spring-managed sind) sind dort noch nicht in dem Zustand, in dem Sie sie bequem nutzen können.
Wenn Sie ein Bild brauchen: main() ist keine Küche und keine Bar, es ist der Lichtschalter am Eingang. Drücken – ok. Borschtsch auf dem Schalter zu kochen – das ist schon seltsam.
2. Runner als Start-Hook
Um main() nicht zu einer „Kommandobrücke für alles“ zu machen, bietet Spring Boot einen sehr klaren und sicheren Mechanismus: Runner. Ein Runner ist eine Spring-managed Komponente (also ein gewöhnlicher Bean), die Boot einmal beim Start der Anwendung aufruft, wenn der Kontext bereits aufgebaut ist und Abhängigkeiten normal injiziert werden können. Es ist wie der „letzte Schritt, bevor man sagt: Die Anwendung ist bereit“.
Es ist wichtig, die Idee zu erfassen: Ein Runner ist kein „alternativer main“. Es ist ein Hook (Haken) im Lebenszyklus, an dem man bequem kurze Startlogik unterbringen kann. Vom Gefühl her ähnelt es dem: „Nachdem alles hochgefahren ist, erledige eine kleine und verständliche Startarbeit“.
Visuell kann man die Abfolge etwa so darstellen:
flowchart TD
A[JVM startet] --> B["main(String... args)"]
B --> C["SpringApplication.run(...)"]
C --> D[Erstellung des ApplicationContext]
D --> E[Erstellung der Beans und Dependency Injection]
E --> F["Aufruf der Runner (1 Mal)"]
F --> G[Anwendung gilt als bereit]
Der entscheidende Punkt hier ist, dass der Runner nach dem Aufbau des Abhängigkeitsgraphen durch Spring gestartet wird. Das bedeutet, dass der Runner als normale Spring-Komponente mit Constructor Injection geschrieben werden kann: Er kann Services, Repositories, Konfigurationen erhalten – alles, was im Kontext vorhanden ist. Aber (und das ist wichtig) der Runner sollte kurz und vorhersehbar bleiben: Es ist ein Teil des Starts, und der Start ist der Ort, an dem alles maximal klar sein sollte, sonst werden Sie den Start debuggen anstatt zu entwickeln.
In Spring Boot gibt es zwei ähnliche Interfaces, die diese Aufgabe lösen: CommandLineRunner und ApplicationRunner. Nun werden wir sie sorgfältig nach ihren Rollen aufteilen.
3. CommandLineRunner: String... args
CommandLineRunner ist der einfachste und geradlinigste Startup-Hook. Er wird in Situationen benötigt, in denen Ihnen entweder die Startargumente egal sind oder Sie sie als rohes String-Array erhalten möchten, ohne „intelligente Zerlegung“. Für einen Anfänger ist das oft der beste Start: einfachere Signatur, weniger neue Konzepte, leichter zu erfassen, dass der Runner wirklich an der richtigen Stelle gestartet wird.
Im Folgenden werden einige kleine Varianten desselben Startup-Hooks gezeigt. Sie müssen nicht alle gleichzeitig im catalog-service behalten werden: Es sind Lernproben, um die Mechanik zu fühlen und die passende Form auszuwählen.
Minimalbeispiel im catalog-service
Stellen wir uns vor, wir wollen beim Start einfach sehen: Die Anwendung hat die Runner-Phase erreicht und wie viele Argumente ihr übergeben wurden. Erstellen wir eine Komponente (Bean), die CommandLineRunner implementiert.
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class RawArgsRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// Boot calls this method once at startup, when the context is already up
System.out.println("Raw args count = " + args.length); // Raw args count = 0
}
}
Beachten Sie zwei Dinge. Erstens: Es ist eine normale Spring-Komponente (@Component), d.h. sie muss nicht manuell erstellt werden. Zweitens: run(...) erhält String... args – genau die Argumente, die die JVM an main() übergibt, und Boot reicht sie in den Start hinein.
Wenn Sie die Anwendung ohne Argumente starten, erhalten Sie 0. Wenn Sie ein paar Argumente hinzufügen (z. B. in der IDE im Feld Program arguments), sehen Sie eine andere Zahl. Wir werden die Anwendung jetzt nicht in eine CLI-Maschine verwandeln, es ist uns wichtig, den Mechanismus zu spüren.
Alternative: Runner über @Bean deklarieren
Manchmal ist der Runner so klein, dass eine separate Klasse „zu feierlich“ erscheint. Man kann ihn als Bean in einer Konfigurationsklasse über @Bean registrieren. Es ist der gleiche Mechanismus, nur eine andere Art der Registrierung.
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StartupConfiguration {
@Bean
CommandLineRunner helloRunner() {
// Convenient variant for very short startup logic without a separate class
return args -> System.out.println("Hello from runner"); // Hello from runner
}
}
Es ist nicht „besser“ oder „schlechter“ – es ist eine Frage der Bequemlichkeit. Für ein Lernprojekt ist es oft einfacher, mit einer separaten Klasse zu beginnen: Sie ist leichter lesbar und einfacher zu erweitern. Aber es ist nützlich zu wissen, dass die @Bean-Variante existiert und durchaus funktionsfähig ist. Wenn Sie diese Variante wählen, wird die vorherige Klasse in der Regel einfach nicht benötigt.
4. ApplicationRunner: ApplicationArguments
ApplicationRunner löst die gleiche Aufgabe wie CommandLineRunner: Ihnen einen Ort für einmalige Startlogik zu geben. Der Unterschied besteht darin, dass er anstelle von rohen Strings ein ApplicationArguments-Objekt erhält – ein Objekt, das die Startargumente speichern kann und eine bequemere API zum Lesen bietet. Dies ist besonders nützlich, wenn Sie „Flags“ von „positionsbasierten Werten“ unterscheiden möchten und keinen manuellen Parser mit zehn if und zwei split("=") schreiben möchten (was normalerweise mit Tränen und seltsamen Bugs endet).
Hier benötigen wir nicht die gesamte API von ApplicationArguments. Es reicht, die Tatsache zu sehen: Ein Runner kann nicht nur String..., sondern auch ein strukturierteres Objekt erhalten.
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class SourceArgsRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// getSourceArgs() is the "raw" source array of strings that came to the application at startup
int count = args.getSourceArgs().length;
System.out.println("Source args count = " + count); // Source args count = 0
}
}
Dieses Beispiel ersetzt normalerweise auch die vorherige Probe, wenn Sie genau ApplicationArguments benötigen, und nicht nur existiert, um eine Sammlung von Runner zu haben.
Dieses Beispiel ist absichtlich „fast gleich“ wie bei CommandLineRunner. Es soll zeigen: Der Mechanismus ist der gleiche, der Startpunkt ist der gleiche, aber die Eingabedaten sind nicht mehr nur ein Array von Strings. Für das tatsächliche Lesen von Optionen hat ApplicationArguments containsOption(...), getOptionValues(...) und getNonOptionArgs(), aber hier ist es uns wichtiger, die Gabelung selbst zu festigen: Es gibt zwei Interfaces, und der Unterschied liegt in der Form der Argumente.
Ein weiterer wichtiger Punkt: ApplicationRunner gewinnt normalerweise gegenüber CommandLineRunner, selbst wenn Sie nicht vorhaben, die Argumente sofort zu lesen. Einfach, weil in einem realen Projekt die Frage „Fügen wir doch eine kleine Startoption hinzu“ eine typische Anfrage ist, und wenn Sie bereits auf ApplicationRunner sind, sind Sie dafür bereit.
5. Auswahl des Runner-Interfaces
Die Wahl zwischen diesen beiden Mechanismen ist meist nicht philosophisch. Sie ist sehr praktisch: Wie wichtig sind Ihnen die Argumente und wie sehr möchten Sie sie „menschenlesbar“ lesen. Innerhalb eines Projekts können sie gemischt werden, aber in der Regel ist es gesünder, sich an eine Logik zu halten: Für eine Aufgabe ein Runner, und es ist besser, wenn er lesbar ist und nicht ein „universelles Kombigerät“.
| Kriterium | |
|
|---|---|---|
| Signatur | run(String... args) | run(ApplicationArguments args) |
| Eingabedaten | rohe Strings | Objekt mit API zum Lesen der Argumente |
| Wann praktisch | „Argumente sind mir egal“ oder „Array of Strings reicht“ | „Ich möchte Argumente ohne manuelles Parsen lesen“ |
| Typischer Komfortlevel im Projekt | Startlevel, am einfachsten | in der Regel langfristig bequemer |
Nun einige „lebensnahe“ Szenarien, in denen Runner wirklich nützlich sind (ohne Ausflüge in zukünftige Themen). Ein Runner eignet sich gut, wenn Sie beim Start eine kurze und vorhersehbare Aktion ausführen möchten. Zum Beispiel in der Konsole ausgeben: „wir sind in der Runner-Phase“ – das ist eine einfache Überprüfung während des Lernens. Oder eine kleine Invariantenprüfung, von der der Sinn des Dienstes abhängt – z.B. sicherstellen, dass das Verzeichnis nicht leer ist.
Aber in der Rolle „ich starte einen großen Prozess für 30 Sekunden beim Start, der über das Netz geht und Dateien an fünf Stellen liest“ wirkt der Runner bereits fragwürdig. Nicht weil es verboten ist, sondern weil Sie den Start verschlechtern und verkomplizieren. Heute wollen wir das Gegenteil: den Start beobachtbar und verständlich machen.
Eine kurze Regel, die hilft, Fehler zu vermeiden: Wenn Sie den Runner mit den Worten „die Anwendung macht beim Start genau diese eine Sache und lebt dann weiter“ beschreiben, sind Sie auf dem richtigen Weg. Wenn Sie sich dabei ertappen zu denken „der Runner ist fast eine separate Subsystem der Anwendung“, dann haben Sie es bereits übertrieben.
Stil der Runner-Logik im catalog-service
Wenn Sie einen Runner in einem Lernprojekt schreiben, passiert es sehr leicht, dass Sie daraus versehentlich eine „Box machen, in die wir alles Unverstandene packen“. Es ist ja die Startlogik – also packen wir Initialisierung, Prüfungen, Parameterausgabe, schnelle Fixes und noch etwas hinein, und … schon haben Sie eine Klasse mit 300 Zeilen, die den Start bei einem überflüssigen Leerzeichen bricht. Deshalb legen wir jetzt den Stil fest, den wir im catalog-service beibehalten werden.
Erstens sollte der Runner explizit benannt sein. Selbst wenn er temporär ist. RawArgsRunner ist für ein Lernprojekt in Ordnung. Für Produktionscode ist etwas wie StartupSummaryRunner, CatalogSeederRunner, StartupChecksRunner besser – damit kaufen Sie sich durch Lesbarkeit Ruhe. Zweitens: Halten Sie den Runner an einem logischen Ort. Für unser Projekt ist das das Paket com.example.catalogservice.catalog.bootstrap: dort werden die einmaligen Dinge leben, die am Start beteiligt sind (bootstrap bedeutet genau „hochfahren und für den Betrieb vorbereiten“).
Drittens: Ein Runner sollte eine Sache tun. Wenn Sie sowohl eine Zusammenfassung ausgeben, kritische Bedingungen prüfen und noch etwas anderes wollen, ist das ein Grund, entweder in zwei Komponenten aufzuteilen oder (öfter) zu überlegen, ob Sie nicht ein echtes Architekturproblem unter den Teppich des Startcodes kehren. Ein gesunder Stil ist hier einfach: kurzer Runner, saubere Prüfungen und keine „zweite Phase der Anwendung“ beim Start.
Und schließlich eine sehr praktische Nuance: Ein Runner ist ein Bean, daher sollte sein Konstruktor langweilig sein. Der Konstruktor ist nur für Abhängigkeiten. Keine Logik wie „lass uns hier gleich etwas berechnen/lesen/starten“ sollte im Konstruktor sein. Jede Aktion gehört in run(...), weil es explizit sagt: „das ist die Startlogik“.
6. Typische Fehler bei der Arbeit mit Runnern
Die ersten Fehler mit Runnern entstehen normalerweise nicht wegen eines „schlechten Programmierers“, sondern aufgrund alter Gewohnheiten: Konsolenanwendungen, manuelle Objektverwaltung, der Wunsch, schnell „eine Sache zu prüfen“ und zu vergessen. Aber Boot als Plattform ist sehr empfindlich, wo Sie den Code platzieren – daher ist es besser, diese Fallen im Voraus zu erkennen.
Fehler Nr. 1: Startlogik in main() schreiben und Abhängigkeiten manuell erstellen.
So verwandeln Sie die Boot-Anwendung schnell in eine Mischung aus „etwas Spring“ und „etwas selbstgemachtem DI“. Dabei entstehen seltsame Duplikate: Einen Teil der Objekte erstellt Spring, einen Teil erstellen Sie selbst. Es ist korrekter, main() maximal kurz zu lassen und die einmalige Startlogik in einen Runner auszulagern, der ein Spring-Bean ist und Abhängigkeiten über den Konstruktor erhalten kann.
Fehler Nr. 2: Das gleiche Verhalten in mehreren Runnern „für alle Fälle“ duplizieren.
Manchmal gibt es zwei Komponenten, die beide die Startargumente ausgeben, beide „gestartet“ ausgeben, beide ähnliche Prüfungen durchführen. Das erzeugt Lärm und Verwirrung: Sie wissen nicht mehr, welcher der „Haupt“-Runner ist und warum sich die Meldungen in der Konsole wiederholen. Versuchen Sie, einen Runner pro verständlicher Aufgabe zu behalten und temporäre Lernrunner zu entfernen oder zusammenzuführen.
Fehler Nr. 3: Startarbeit im Konstruktor des Beans verstecken.
Der Konstruktor wird beim Erstellen des Beans aufgerufen, und wenn Sie dort Logik einfügen, wird sie nebenbei ausgeführt, ohne einen expliziten Platz im Lebenszyklus. Das erschwert das Debuggen: Sie sehen einen Startfehler, wissen aber nicht, in welcher Phase er aufgetreten ist. Lassen Sie den Konstruktor nur Abhängigkeiten entgegennehmen und die echte Startlogik in run(...) leben.
Fehler Nr. 4: Den Runner zu lang und unvorhersehbar machen.
Der Runner sollte kurz sein, weil er auf dem kritischen Pfad des Starts liegt. Wenn darin beginnt „verbinde mit dies“, „lies das“, „berechnene jenes“, erhalten Sie einen langsamen und fragilen Start. Besonders unangenehm ist es, wenn der Start manchmal schnell und manchmal langsam ist – solche Probleme sind später sehr schwer zu finden. Besser mehrere kleine, klare Aktionen als eine „Start-up-Monster“.
Fehler Nr. 5: Den Runner als Ort für „Hintergrundaufgaben“ betrachten.
Der Runner wird einmal beim Start ausgeführt und im selben Thread, der die Startup-Sequenz abschließt. Es ist kein „Scheduler“ und kein „Hintergrundarbeiter“. Wenn Sie etwas Periodisches oder Asynchrones benötigen, ist das eine andere Diskussion (und eindeutig nicht das Thema des heutigen Tages). Hier behalten wir den Fokus: Runner sind für einmalige Startlogik und Früherkennung, nicht für das „Leben der Anwendung nach dem Start“.
GO TO FULL VERSION