1. Wenn Stereotype-Annotationen nicht ausreichen
Component Scanning wirkt wirklich wie ein Märchen: Du setzt @Service, startest die Anwendung, und irgendwie „klappt es von selbst". Aber recht schnell findet sich ein Objekt, das man nicht mit einer Annotation markieren kann oder will. Meistens ist das eine Klasse aus dem JDK oder einer Drittbibliothek, die man nicht kontrolliert. Und genau dann verwandelt sich das Märchen in einen kurzen Thriller: „Wo ist meine Bean?"
Stellen wir uns vor, dass wir in unserem Lern-catalog-service die „aktuelle Zeit" nicht direkt über Instant.now() abrufen wollen, sondern über eine Clock. Das ist eine normale Java-Klasse, sehr praktisch für Tests und vorhersehbares Verhalten. Wir schreiben einen Service, der Clock im Konstruktor verlangt — nach den Regeln von DI alles schön und transparent:
package com.example.catalogservice.catalog.service;
import java.time.Clock;
import org.springframework.stereotype.Service;
@Service
public class CourseCatalogService {
private final Clock clock;
public CourseCatalogService(Clock clock) {
// Die Abhängigkeit kommt von außen — so lässt sich leichter testen und die Zeitquelle wechseln
this.clock = clock;
}
}
Und dann sagt Spring ehrlich: „Ich helfe gern, aber… Clock habe ich nicht gesehen, nicht erstellt und höre davon zum ersten Mal." Du bekommst beim Start einen Fehler: Der Container kann den Abhängigkeitsgraph nicht zusammenbauen.
Und das ist normal: Spring ist kein Gedankenleser. Er ist nicht verpflichtet zu raten, ob du Clock.systemUTC() oder Clock.systemDefaultZone() benötigst.
In diesem Moment kommen zwei wichtige Spring Core-Werkzeuge ins Spiel, die wir heute besprechen:
- @Configuration — eine Klasse, die beschreibt, „wie ein Teil der Anwendung zusammengebaut werden soll";
- @Bean — eine Factory-Methode, die ein Objekt explizit als Bean registriert.
Um den Unterschied leichter zu verstehen, hier ein kleines Schema:
flowchart TD
A["Klasse mit @Service/@Component"] -->|component scanning| C["BeanDefinition"]
B["@Bean-Methode in @Configuration"] -->|explizite Registrierung| C
C --> D["ApplicationContext: speichert Beans und Verknüpfungen"]
Stereotypen sind der Weg „Spring, finde mir diese Klassen anhand der Markierungen". @Bean ist der Weg „Spring, hier ist ein konkretes Objekt, mach es zur Bean".
2. @Configuration: die Rezept-Klasse für den Container
Wenn Stereotype-Annotationen „Mitarbeiter-Badges" sind, dann ist @Configuration das „Organigramm und der Montageplan". Wir sagen Spring nicht „suche jemanden Passenden", sondern: „Hier beschreibe ich explizit, welche Objekte die Anwendung benötigt und wie sie erstellt werden sollen." Das ist besonders nützlich, wenn man Bibliotheks-Klassen registriert oder die Erstellung von Objekten kontrollieren möchte.
Technisch ist @Configuration eine normale Java-Klasse ohne magisches DSL, die in deinem Projekt lebt, aber eine infrastrukturelle Rolle erfüllt: Beans beschreiben. Darin befinden sich normalerweise Methoden mit @Bean.
Das verständlichste Beispiel — die Registrierung von Clock:
package com.example.catalogservice.config;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TimeConfiguration {
@Bean
Clock systemClock() {
// Wir wählen die Zeitquelle explizit: UTC ist praktisch für reproduzierbare Tests und Logs
return Clock.systemUTC();
}
}
Beachte einen wichtigen Punkt: Clock ist nicht unsere Klasse. Wir können und sollen nicht ins JDK gehen und dort @Component hinzufügen. Deshalb erstellen wir unsere eigene Konfigurations-Klasse und beschreiben die Bean-Erstellung über eine Factory-Methode.
Ein weiterer praktischer Aspekt, den man oft übersieht: Die Konfigurations-Klasse muss auch von Spring gefunden werden. Also muss sie in der Component-Scanning-Zone liegen — unter dem Root-Paket, von dem aus die Anwendung ihre Komponenten scannt. Deshalb werden wir in catalog-service Konfigurationen in einem Paket wie com.example.catalogservice.config halten.
3. @Bean: eine Objekt-Factory, die Spring steuert
Wenn du @Bean auf eine Methode setzt, teilst du dem Container mit: „Das Ergebnis dieser Methode ist eine Bean. Erstelle sie und verwalte sie." Wichtig ist, @Bean nicht als „Annotation der Schönheit" zu sehen, sondern als offiziellen Einstiegspunkt eines Objekts in die Spring-Welt. Danach lebt das Objekt im ApplicationContext und kann über DI in andere Beans injiziert werden.
Normalerweise verwendet man @Bean für zwei Arten von Dingen. Erstens für Drittklassen wie Clock. Zweitens für eigene Klassen, aber mit nicht-standardmäßiger Erstellungslogik: Zum Beispiel wenn du das Objekt über eine Factory-Methode erstellen, eine Implementierung per Interface zurückgeben oder ein Objekt mit vorab konfigurierten Parametern konstruieren möchtest.
Machen wir eine kleine Prüfung: Überzeugen wir uns, dass Clock wirklich eine Bean geworden ist und aus dem Context abgerufen werden kann. Das ist kein Beispiel für „so macht man es in Produktion", sondern eine Demonstration: Der Context speichert Beans.
package com.example.catalogservice;
import java.time.Clock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(CatalogServiceApplication.class, args);
// Demonstration: Bean manuell aus dem Context holen (im Produktionscode macht man das normalerweise nicht)
Clock clock = context.getBean(Clock.class);
System.out.println(clock.getZone()); // Z
}
}
clock.getZone() gibt Z aus, weil wir Clock.systemUTC() registriert haben.
Noch ein Detail, das hilft, die Magie zu „entmystifizieren": Eine Bean hat einen Namen. Standardmäßig wird er aus dem Methodennamen übernommen. In unserem Fall heißt die Bean systemClock. Manchmal ist das praktisch — etwa für die Diagnose oder wenn man bewusst mit Namen arbeitet.
Wenn man den Namen explizit festlegen muss, kann man es so machen — kurz gezeigt, ohne in „Namensdebatten" einzutauchen:
package com.example.catalogservice.config;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TimeConfiguration {
@Bean("utcClock")
Clock systemClock() {
// Bean-Name wird manuell gesetzt (nützlich, wenn du bewusst mit mehreren Kandidaten arbeitest)
return Clock.systemUTC();
}
}
Aber im normalen Alltag — und auch in unserem Kurs — verlassen wir uns hauptsächlich auf Injektion nach Typ, nicht auf Strings mit Namen.
4. DI in @Bean: Parameter als Abhängigkeiten
An dieser Stelle passiert normalerweise ein angenehmes „Klick": DI funktioniert nicht nur in Konstruktoren von @Service-Klassen, sondern direkt in @Bean-Methoden. Das heißt, @Bean ist nicht nur „erstelle ein Objekt", sondern „erstelle ein Objekt im Kontext des Abhängigkeitsgraphen". Spring kann Methodenparameter genauso mit anderen Beans befüllen wie Konstruktorparameter.
Stellen wir uns vor, wir wollen einen Service ohne @Service erstellen — einfach eine normale Java-Klasse — und ihn explizit über @Bean registrieren. Zum Beispiel um die Erstellung etwas besser zu kontrollieren oder um Studierenden zu zeigen, dass „Bean ≠ Klasse".
Zuerst der Service selbst — eine normale Java-Klasse:
package com.example.catalogservice.catalog.service;
import java.time.Clock;
public class CourseCatalogService {
private final Clock clock;
public CourseCatalogService(Clock clock) {
// Alles Nötige für die Arbeit kommt über den Konstruktor
this.clock = clock;
}
}
Jetzt registrieren wir ihn über @Configuration:
package com.example.catalogservice.config;
import com.example.catalogservice.catalog.service.CourseCatalogService;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CatalogServiceConfiguration {
@Bean
CourseCatalogService courseCatalogService(Clock clock) {
// Spring injiziert die Clock-Bean selbst als Methodenparameter
return new CourseCatalogService(clock);
}
}
Schau dir die Signatur an: courseCatalogService(Clock clock). Wir erstellen Clock nicht innerhalb des Services über Clock.systemUTC() und verstecken die Abhängigkeit nicht. Wir bitten um Clock als Abhängigkeit, und Spring injiziert die passende Bean.
Das ist eine sehr wichtige ingenieurmäßige Gewohnheit: Alles, was ein Objekt zum Arbeiten braucht, muss von außen kommen. Egal, über welchen Weg — den Konstruktor einer Komponente oder die Parameter einer @Bean-Factory-Methode.
Als Mini-Graph dargestellt ergibt das fast eine Schulzeichnung, aber sie hat Kraft:
flowchart LR
Clock["Clock (bean)"] --> Svc["CourseCatalogService (bean)"]
Conf["@Configuration"] -->|beschreibt| Clock
Conf -->|beschreibt| Svc
Die Konfigurations-Klasse ersetzt nicht den Container. Sie teilt dem Container lediglich mit: „Wir haben solche Objekte, und so werden sie zusammengebaut."
5. Mini-Beispiel für catalog-service
Damit das Thema nicht „im Vakuum" bleibt, binden wir es an unser Durchgangsprojekt catalog-service. Selbst in einem kleinen Lern-Service ohne Datenbank und ohne Security gibt es infrastrukturelle Objekte, die sich als Beans bequem halten lassen. Das schafft einen einheitlichen Umgang mit ihnen, verbessert die Testbarkeit und mindert die Versuchung, „irgendwo nebenbei new zu schreiben, damit es einfach läuft".
Erstellen wir eine Konfigurations-Klasse, die Clock und z. B. einen DateTimeFormatter für die einheitliche Datumsformatierung an einer Stelle registriert. Beide Klassen gehören nicht uns, werden aber oft gebraucht:
package com.example.catalogservice.config;
import java.time.Clock;
import java.time.format.DateTimeFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CommonInfrastructureConfiguration {
@Bean
Clock systemClock() {
// Einheitliche Quelle für „aktuelle Zeit" für die gesamte Anwendung
return Clock.systemUTC();
}
@Bean
DateTimeFormatter isoDateFormatter() {
// Einheitlicher Formatter — damit keine „eigenen Regeln" über den ganzen Code verteilt werden
return DateTimeFormatter.ISO_LOCAL_DATE;
}
}
Jetzt kann jede Komponente — oder eine über @Bean erstellte Bean — Clock und DateTimeFormatter über den Konstruktor anfordern. Machen wir z. B. einen kleinen „Helfer" für die schöne Formatierung des Kurs-Startdatums. Er braucht einen externen DateTimeFormatter — genau der Fall, wo @Bean auch für eigene Klassen praktisch ist:
package com.example.catalogservice.catalog.support;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class LaunchDateFormatter {
private final DateTimeFormatter formatter;
public LaunchDateFormatter(DateTimeFormatter formatter) {
// Abhängigkeit — externer Formatter, nicht „fest verdrahtet" innerhalb der Klasse
this.formatter = formatter;
}
public String format(LocalDate date) {
// Der Sinn der Klasse — Daten einheitlich formatieren
return formatter.format(date);
}
}
Und registrieren wir ihn:
package com.example.catalogservice.config;
import com.example.catalogservice.catalog.support.LaunchDateFormatter;
import java.time.format.DateTimeFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CatalogSupportConfiguration {
@Bean
LaunchDateFormatter launchDateFormatter(DateTimeFormatter isoDateFormatter) {
// Der Methodenparameter ist eine Abhängigkeit, die Spring bereitstellt
return new LaunchDateFormatter(isoDateFormatter);
}
}
Das sieht wie „manuelle Montage" aus, aber der wichtigste Unterschied zur manuellen Montage in main() aus der ersten Vorlesung besteht darin, dass das jetzt im Container lebt. Das heißt: zentralisierte Objekterstellung, einheitlicher Abhängigkeitsgraph und die Möglichkeit, DI im weiteren Projektverlauf problemlos zu nutzen.
6. Thematische Konfigurationen statt „God-Config"
Wenn ein Entwickler @Configuration und @Bean erst versteht, kommt oft die Versuchung: „Oh! Das heißt, ich kann die gesamte Anwendung an einem Ort zusammenbauen." Und ein paar Tage später erscheint im Projekt eine Datei AppConfiguration.java mit 800 Zeilen, in der Uhren, Formatter, Services, Controller, „temporäre Krücken für eine Minute" und eine mysteriöse private static final String DO_NOT_TOUCH = "es funktioniert" nebeneinander wohnen.
Damit unser catalog-service lesbar bleibt, gilt eine sehr einfache Regel: Eine Konfiguration muss thematisch sein. Nicht „alle Konfigurationen der Welt", sondern „Zeit-Konfiguration", „Katalog-Konfiguration", „Hilfskomponenten-Konfiguration". Das ist kein architektonisches Pathos, sondern einfache Fürsorge für die Psyche des künftigen Lesers — einschließlich dir selbst in zwei Wochen.
Beispiel einer gesunden Richtung für die Struktur — vorerst sehr vereinfacht, ohne künftige Kurs-Layer:
com.example.catalogservice/
|-- CatalogServiceApplication
`-- config/
|-- CommonInfrastructureConfiguration
|-- CatalogServiceConfiguration
`-- CatalogSupportConfiguration
Warum das besonders für Junior-Niveau gut funktioniert:
1) Du findest schnell, wo Clock erstellt wird — in der Infrastruktur-Konfiguration — statt ein find in files über das gesamte Projekt zu machen;
2) Konfigurations-Klassen bleiben klein, und man kann den Code tatsächlich lesen, statt nur zu weinen;
3) Es ist leichter, die Verantwortung zu verstehen: @Configuration ist „Objekt-Montage", kein Ort für Geschäftslogik.
Noch ein nützlicher Grundsatz: Eine @Bean-Methode sollte kurz und vorhersehbar sein. Wenn darin komplexe Logik beginnt — Schleifen, Netzwerkanfragen, Datei-Lesen, Berechnungen —, hast du meistens Geschäftslogik an den falschen Ort verschoben. Konfiguration ist keine Küche für ein sechsstündiges Mittagessen. Eher wie eine Kaffeemaschine: Knopf drücken — Tasse bekommen. Schnell und zuverlässig.
7. Typische Fehler beim Arbeiten mit @Configuration und @Bean
Fehler mit @Configuration und @Bean haben meist nicht mit der „falschen Annotation" zu tun, sondern mit einem falschen Bild im Kopf: Man denkt, Spring sei ein Haufen Aufkleber und der Container nur eine Bibliothek. Deshalb sind das unten keine „peinlichen Anfängerfehler", sondern sehr verbreitete Fettnäpfchen, in die fast alle treten — auch Leute, die später schreiben werden „10 Jahre Berufserfahrung".
Fehler Nr. 1: Vergessen, dass die Konfigurations-Klasse auch ins Component Scanning gelangen muss.
Du kannst @Configuration und @Bean perfekt schreiben, aber die Konfiguration in einem Paket ablegen, das nicht gescannt wird, oder sie einfach in einem „falschen" Modul/Ordner erstellen — und Spring sieht sie nicht. Von außen wirkt es so, als wäre @Bean „nicht angesprungen", aber in Wirklichkeit weiß der Container schlicht nicht, dass diese Klasse existiert.
Fehler Nr. 2: @Configuration über new erstellen und erwarten, dass Spring sich magisch einklinkt.
Manchmal möchte man „schnell" so vorgehen: new TimeConfiguration().systemClock(). Das erstellt eine Clock, aber es ist ein normales Objekt, keine Bean, und der Container erfährt davon nichts. Das häufigste Problem klingt dann so: „Ich habe doch Clock erstellt… warum beschwert sich Spring immer noch, dass keine Bean vorhanden ist?" Weil der Container nur sieht, was im Container registriert ist.
Fehler Nr. 3: Abhängigkeiten in Klassen verstecken, obwohl es bereits eine Bean gibt.
Klassiker: Du hast bereits @Bean Clock systemClock() gemacht, aber im Service steht trotzdem private final Clock clock = Clock.systemUTC();. Damit gibt es zwei verschiedene Quellen für „aktuelle Zeit": eine aus dem Container, eine selbst gebaut. Das bricht die Idee von DI: Das Objekt hat sich seine Abhängigkeit selbst zusammengebaut, und das ist von außen nicht sichtbar.
Fehler Nr. 4: @Configuration zum Ort für Geschäftslogik machen.
Wenn du in einer @Bean-Methode anfängst, Kurse zu filtern, Statistiken zu berechnen oder HTML zu bauen — ja, auch das passiert —, vermischst du die Montage der Anwendung mit der Logik der Anwendung. Das macht die Konfiguration unvorhersehbar und schwer, der Start der Anwendung verlangsamt sich, und das Debuggen ist schmerzhafter als ein Merge-Konflikt in YAML.
Fehler Nr. 5: Versehentlich zwei Beans desselben Typs registrieren und sich über Mehrdeutigkeit wundern.
Heute besprechen wir noch nicht die Mechanismen zur Bean-Auswahl bei mehreren Kandidaten, aber das allgemeine Prinzip ist jetzt schon nützlich zu kennen: Wenn du zwei Clock-Beans erstellst — z. B. eine UTC, eine systemDefault —, kann Spring nicht „raten", welche genau in CourseCatalogService injiziert werden soll, und die Anwendung stürzt beim Start ab. Das ist keine Boshaftigkeit von Spring, sondern eine ehrliche Anforderung: Die Wahl muss eindeutig sein.
GO TO FULL VERSION