1. Manuelle Bean-Registrierung: wann sie nervt
Wenn du bisher alles „richtig" gemacht hast, fängt dein AppConfig oder Startcode schon an, wie eine Wocheneinkaufsliste auszusehen: Alles davon ist irgendwie nützlich, aber es wächst schneller als das Gefühl der Kontrolle. Der Grund ist einfach und sehr ingenieurmäßig: Man muss dem Container mitteilen, welche Klassen als Beans gelten sollen. Manuell funktioniert das, aber der Spaß endet, sobald das Projekt ein bisschen größer als „Hello, World" wird.
Erinnere dich an unser ContextFlow. Selbst in der Lernversion erscheinen dort sehr schnell Szenario-Services, Bestellspeicher, Audit, Benachrichtigungen, ID-Generator und so weiter. Wenn man jede Klasse manuell auflistet, wird die Änderung „eine Klasse hinzugefügt" schnell zu „Klasse hinzugefügt, nicht vergessen zu registrieren, Import korrigiert, Namen nicht verwechselt". Das ist noch keine Katastrophe, aber bereits die richtige Zeit, um zu schauen, wie Spring diese Aufgabe in Alltagsprojekten normalerweise löst.
Um den Kontrast zu sehen, hilft eine einfache Tabelle im Kopf – sie ersetzt lange Listen und spart ein paar Nervenzellen:
| Frage | Manuelle Registrierung | Component Scanning |
|---|---|---|
| Wo „wohnt" die Registrierungsinformation? | In der Konfiguration (oft weit weg von der Klasse) | Direkt neben der Klasse (Annotation auf der Klasse) |
| Was ändert sich beim Hinzufügen eines neuen Services? | Man muss nicht vergessen, „in die Config einzutragen" | Es reicht, die Klasse zu markieren und ins richtige Package zu legen |
| Risiko „vergessen zu registrieren" | Hoch | Geringer (aber andere Risiken entstehen: Scan-Grenzen) |
| Was tut der Container? | Registriert das, was du explizit aufgelistet hast | Sucht selbst nach Kandidaten im angegebenen Bereich |
Die heutige Idee ist sehr einfach: Wir wechseln nicht zu einem anderen Spring. Wir ändern nur die Art der Registrierung von Beans: statt „manuell aufzählen" – „automatisch nach Regeln finden".
2. Component Scanning: Registrierung automatisieren
Der Begriff „Scanning" klingt, als würde Spring einen Laserscanner nehmen, durchs Projekt laufen und die Gedanken des Entwicklers lesen. Zum Glück oder Leidwesen ist es nüchterner. Component Scanning ist ein sehr pragmatischer Mechanismus: Der Container bekommt einen Suchbereich (normalerweise das Root-Package der Applikation) und sucht dort nach Klassen, die mit speziellen Annotationen markiert sind, um sie als Beans zu registrieren.
Wichtig: Die Hälfte der „Magie" sofort ausräumen: Scanning löst die Aufgabe der Registrierung, nicht Fragen wie „Wie Abhängigkeiten einsetzen", „Wie eine Interface-Implementierung wählen" oder „Wie mit Profilen leben". Wir brauchen heute nur das minimale Arbeitsmodell:
Klasse markieren → Suchbereich festlegen → Kontext hochfahren → Bean erhalten
Das ist alles. Keine geheimen Portale zu Boot und kein „Es macht sich irgendwie von selbst". Liegt die Klasse außerhalb des Suchbereichs, kann man sie mit Annotationen bekleben so viel man will – der Container erfährt nichts davon. Und umgekehrt: Ist der Suchbereich zu weit gefasst, kann der Container mehr finden, als erwartet, und man liest wieder den Stack Trace – nur diesmal mit einem neuen Plot.
Es gibt auch eine wichtige Verbindung zu den vergangenen Tagen: Eine gefundene Klasse wird nicht sofort zum fertigen Objekt. Zuerst findet Spring sie als Kandidaten, registriert BeanDefinition, und erst dann – je nach Start-Strategie – erstellt er echte Instanzen. Das ist derselbe zweiphasige Ansatz, den wir bereits in Level 4 besprochen haben, nur dass die Quelle für BeanDefinition jetzt Scanning ist und keine manuelle Konfiguration.
3. @Component: eine Klasse als Bean-Kandidat
Die Annotation @Component ist das Basis-Label, mit dem wir Spring sagen: „Diese Klasse hier ist ein Kandidat für die Registrierung als Bean." Sie befindet sich im Package org.springframework.stereotype und ist inhaltlich maximal neutral: „Komponente der Applikation". Später sehen wir spezifischere Stereotype-Annotationen, aber heute behalten wir den Fokus auf der Basis.
Wichtig: Zwei Aussagen nicht verwechseln. Richtig: „Eine Klasse mit @Component kann eine Bean werden." Falsch: „Eine Klasse mit @Component ist bereits eine Bean." Dazwischen liegt noch eine wichtige Sache – @ComponentScan, die den Suchbereich festlegt. Ohne Scanning ist @Component wie ein Aufkleber „Wichtig" auf einem Dokument, das in einer Schublade liegt, in die niemand schaut.
Ein Minimalbeispiel für ContextFlow: Nehmen wir an, wir haben den Port AuditWriter, und die erste Implementierung schreibt das Audit in die Konsole. Das ist ein hervorragender Komponentenkandidat: Ein Infrastrukturobjekt, das im Container leben soll.
import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Component;
// Klasse als Komponente markieren: beim Scanning registriert Spring sie als Bean
@Component
public class ConsoleAuditWriter implements AuditWriter {
@Override
public void write(String message) {
// Einfachste Implementierung: Audit in die Konsole schreiben (Infrastrukturcode)
System.out.println("[AUDIT] " + message); // [AUDIT] ...
}
}
Achte auf den Sinn dieses Code-Stücks: Wir erstellen kein Objekt, rufen kein new auf und rühren den Kontext nicht an. Wir markieren die Klasse lediglich – das heißt, wir beschreiben eine Absicht: „Container, bitte achte darauf, falls du scannen solltest."
Und noch ein sanftes, aber wichtiges Stopp-Signal: Nicht alles zur Bean machen. Domain-Klassen wie Order, OrderItem, Kommandos wie CreateOrderCommand – das sind normalerweise gewöhnliche Objekte, die im Laufe eines Szenarios erstellt werden. Der Container ist kein Lager für alles Mögliche, sondern ein Manager für Infrastruktur und Services.
4. @ComponentScan: die Suchgrenze
Wenn @Component ein Label auf einer Klasse ist, dann ist @ComponentScan die Regel für den Container: Wo solche Klassen gesucht werden sollen. Meistens platziert man @ComponentScan auf der Konfigurationsklasse – der, mit der man den ApplicationContext hochfährt. Das ist logisch: Die Konfiguration ist der Zusammenbau-Punkt der Applikation, und Scanning ist Teil dieses Zusammenbaus.
Die minimale Konfiguration für ContextFlow sieht so aus: Wir sagen Spring, dass das Root-Package des Projekts com.example.contextflow ist, und genau das soll gescannt werden.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
// Konfigurationsklasse: über sie geben wir Spring die Start-Anweisungen
@Configuration
// Scan-Grenze: Spring sucht Komponenten innerhalb des angegebenen Packages und seiner Subpackages
@ComponentScan(basePackages = "com.example.contextflow")
public class AppConfig {
}
Wichtig: Nicht am Wort @Configuration hängenbleiben. Heute analysieren wir keine „Konfigurations-Modi" und keine Feinheiten von @Bean-Methoden. Es reicht die einfache Idee: Das ist die Klasse, die du an den AnnotationConfigApplicationContext übergibst, und über sie bekommt Spring die Start-Anweisungen. Eine davon lautet: „Scanning einschalten."
Danach ändert sich unser bekannter Start-Code kaum: Wir erstellen den Kontext wie gehabt, können getBean() aufrufen wie gehabt, und schließen den Kontext korrekt über try-with-resources.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApplication {
public static void main(String[] args) {
// try-with-resources stellt das korrekte Schließen des Kontexts sicher
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Wenn der Kontext hochgefahren ist – Config wurde gelesen und Scanning hat funktioniert
System.out.println("Context started"); // Context started
}
}
}
An dieser Stelle entsteht bei Einsteigern oft die Erwartung: „Na, ich habe @ComponentScan gesetzt, also hat Spring alles im Universum gefunden." Nein. Es hat alles innerhalb der gesetzten Grenze gefunden – und nur das, was den Kriterien für eine „Komponente" entspricht (in unserem Fall: entsprechend annotiert ist).
5. Kontextstart: Scanning → BeanDefinition → Objekt
Solange wir die Kette „was nach was" nicht festhalten, bleibt Scanning Magie. Tatsächlich fügt es sich perfekt in das schon bekannte Modell aus den vorherigen Tagen ein: Spring sammelt gerne erst Metadaten und erstellt dann Objekte. Component Scanning wird dabei einfach zu einer der Quellen dieser Metadaten.
Stell dir vor, AppConfig ist eine „Stadtkarte" und @Component ist ein „Schild am Haus". Wenn der Container startet, nimmt er ungefähr diesen Weg: Karte bekommen, Viertel durchgehen, Schilder sehen, Häuserliste erstellen – und erst dann anfangen, „Mieter einzuziehen" – also Objekte zu erstellen und ihre Abhängigkeiten zu verknüpfen.
Ein kleines Schema, das normalerweise gut im Kopf landet (und den Drang vermindert, an Feen zu glauben):
flowchart TD
A[AppConfig] --> B["@ComponentScan: basePackages"]
B --> C[Classpath-Scanning: Klassen suchen]
C --> D[BeanDefinition registrieren]
D --> E[Singleton-Beans erstellen]
E --> F[Fertiger ApplicationContext]
Das Wichtigste hier: Verstehen, dass Scanning kein „Objekt erstellt". Es hilft dem Container, eine Liste dessen aufzubauen, was erstellt werden muss. Danach greifen die bekannten Mechanismen: Lebenszyklus, Dependency Injection usw. Heute gehen wir nicht tiefer in den Lebenszyklus, aber die Phasen sollte man trotzdem nicht durcheinanderbringen.
Wenn man das mit den eigenen Händen sehen möchte – was meistens besser verständlich macht –, kann man eine einfache Diagnose machen: Den Kontext fragen, ob eine Bean per Name registriert ist. Das ist kein „richtiger Business-Code", aber fürs Lernen und Debuggen – genau das Richtige.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Der Standardname für ConsoleAuditWriter lautet "consoleAuditWriter"
System.out.println(context.containsBean("consoleAuditWriter")); // true
}
Der Sinn dieses Fragments liegt nicht in containsBean(). Der Sinn ist: Nach dem Scanning hat der Kontext tatsächlich einen Eintrag für die Komponente – und das ist keine Vermutung, sondern eine Tatsache, die man prüfen kann.
6. Bean-Namen: Regeln und Kontrolle
Sobald Scanning eingeschaltet ist, entsteht eine praktische Frage: „Okay, der Container hat die Klasse gefunden. Wie hat er sie benannt?" Das ist kein müßiges Interesse. Auch wenn man Beans normalerweise nach Typ holt, tauchen Namen in der Diagnose, in Fehlermeldungen, in Logs auf – und später in komplexeren Kandidatenauswahlszenarien.
Standardmäßig generiert Spring den Bean-Namen aus dem Klassennamen, wobei der erste Buchstabe kleingeschrieben wird. Also wird ConsoleAuditWriter zu consoleAuditWriter. Das klingt logisch und reicht in den meisten Fällen.
Wenn der Name aber wichtig ist – etwa weil man eine stabile Bean-Bezeichnung für Lernzwecke braucht oder für eine spätere „explizite Wahl" –, kann man ihn direkt in @Component manuell setzen. Das geschieht über den Parameter value.
import org.springframework.stereotype.Component;
// Bean-Name explizit setzen (praktisch, wenn man später einen Lookup nach Name macht)
@Component("orderPrinter")
public class OrderPrinter {
public String print(String orderId) {
// Einfacher Service: gibt einen String für die Bestellausgabe zurück
return "Order: " + orderId;
}
}
Jetzt kann man ihn nach Name holen:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Bean nach Name und Typ holen (sicherer als nur nach Name)
var printer = context.getBean("orderPrinter", OrderPrinter.class);
System.out.println(printer.print("ORD-1")); // Order: ORD-1
}
Wichtig: Nicht ins andere Extrem verfallen – Namen nicht überall manuell setzen „auf Vorrat". In kleinen Applikationen und Lernbeispielen ist das manchmal nützlich, aber im echten Code ist es oft bequemer, Abhängigkeiten nach Typ zu bekommen. Unser Ziel auf dieser Stufe: Verstehen, dass eine Bean einen Namen hat, woher er kommt und wie man ihn vorhersehbar macht, wenn es irgendwann nötig ist.
Und ja, eine kleine Notiz für die Zukunft – ohne Details: Namen werden besonders interessant, wenn man mehrere Implementierungen eines Interfaces hat. Aber dorthin gehen wir heute nicht – heute lernen wir, damit wir später nicht leiden.
7. Minimalbeispiel für ContextFlow
Zum Festigen der Mechanik machen wir ein kleines, aber vollständiges Stück. Nicht „die gesamte Applikation übertragen", sondern „einen minimalen Schritt machen, der sicher funktioniert". In Lernprojekten ist das fast immer der beste Weg, sich nicht zu verlieren: in kleinen, prüfbaren Schritten vorgehen.
Für die Scanning-Mechanik selbst brauchen wir jetzt kein vollständiges ContextFlow-Szenario. Ein kleiner ScenarioRunner ohne Abhängigkeiten reicht: Hier ist es wichtig, nicht den finalen Projektbau zu sehen, sondern die bloße Tatsache, dass der Container die Klasse gefunden hat und sie nach Typ zurückgeben kann.
import org.springframework.stereotype.Component;
// Komponente, die beim Scanning gefunden und im Kontext registriert werden soll
@Component
public class ScenarioRunner {
public void run() {
// Demo-Effekt: zeigt, dass die Bean wirklich erstellt wurde und die Methode ausgeführt wird
System.out.println("ContextFlow started"); // ContextFlow started
}
}
Das Scanning ist in AppConfig eingeschaltet (haben wir bereits gezeigt). Dann bleibt der Start so:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApplication {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Bean nach Typ holen: Der Name ist hier nicht wichtig, wichtig ist, dass er im Kontext ist
var runner = context.getBean(ScenarioRunner.class);
runner.run(); // ContextFlow started
}
}
}
Und hier – der Schlüsselerfolg des Tages: ScenarioRunner landete im Kontext nicht, weil wir ihn explizit registriert haben, sondern weil der Container ihn beim Scanning gefunden hat. Genau dafür war das alles gedacht. Wir reduzieren den Konfigurations-Code und bringen die Registrierungsinformation näher an die Klasse selbst.
Wichtig: Einen einfachen Gedanken festhalten: Der ApplicationContext ist weiterhin dasselbe Zentrum des Systems. Wir haben „Spring nicht umgangen", wir haben dem Container einfach beigebracht, einen Teil der Beans automatisch zu finden.
Scanning-Ergebnis prüfen
Im echten Leben ist nach dem Einschalten des Scannings die häufigste Frage nicht philosophisch, sondern sehr praktisch: „Warum findet er meine Komponente nicht?" Oder, wenn der Tag nicht gut läuft: „Warum hat er etwas gefunden, das ich überhaupt nicht sehen wollte?" Daher ist es an dieser Stelle nützlich, ein paar Diagnose-Tricks zu kennen – ohne sie zu einem eigenen Programmierstil zu machen.
Eine der einfachsten Methoden: Die Liste der Bean-Definition-Namen ausgeben. Ja, das ist laut, aber man sieht sofort: Hat Scanning überhaupt etwas registriert oder nicht? Und man kann schnell prüfen, ob der Name der eigenen Komponente aufgetaucht ist.
import java.util.Arrays;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Diagnose: alle registrierten Bean-Definition-Namen ausgeben
System.out.println(Arrays.toString(context.getBeanDefinitionNames())); // [..., scenarioRunner, ...]
}
Eine weitere ruhige Variante: Das Vorhandensein eines bestimmten Typs prüfen. Das ist oft angenehmer, weil man in Klassen denkt, nicht in Strings.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Schnelle Prüfung: wenn getBean keine Exception geworfen hat, wurde die Komponente gefunden
System.out.println(context.getBean(ScenarioRunner.class) != null); // true
}
Diese Checks sind nicht für die Produktion gedacht. Der Sinn: Man hat einen Schraubenzieher zur Diagnose. Wenn nach dem Hinzufügen von @Component und @ComponentScan die Klasse nicht gefunden wird, prüft man zuerst die Scan-Grenze und das Package – und greift nicht in die Geschäftslogik und beginnt nicht, Java einer Verschwörung zu verdächtigen.
Und eine kurze Merkliste: Wenn eine Komponente nicht gefunden wird, sind meistens drei Dinge schuld – kein @ComponentScan, falsches basePackages, oder die Klasse liegt nicht dort, wo man denkt (z.B. nach einem Package-Refactoring). Alles andere passiert seltener und kommt normalerweise später, wenn das Dependency Assembly komplexer wird.
8. Typische Fehler bei @Component und @ComponentScan
Beim ersten Kennenlernen wirkt Component Scanning zu simpel: „Na was ist da, zwei Annotationen." Genau deshalb sind die Fehler besonders ärgerlich: Man meint, alles gemacht zu haben, aber der Container verhält sich, als würde der eigene Code nicht existieren. Schauen wir uns die häufigsten Patzer nicht nach Exception-Namen an, sondern nach Symptomen – so merkt man sie sich besser.
Fehler Nr. 1: @Component setzen, aber vergessen, Scanning einzuschalten.
Das ist der Klassiker. Man hat ScenarioRunner als @Component markiert, aber @ComponentScan erscheint nicht in der Konfiguration (oder die Konfigurationsklasse wird beim Kontextstart gar nicht verwendet). Das Symptom ist meistens einfach: NoSuchBeanDefinitionException bei getBean(ScenarioRunner.class). Die Lösung ist ebenfalls einfach: Sicherstellen, dass der Kontext genau mit dem AppConfig startet, das @ComponentScan enthält.
Fehler Nr. 2: Denken, dass @Component „ein Objekt sofort erstellt".
Manchmal erwartet man, dass irgendwo in der JVM ein Klicken erklingt, sobald man @Component schreibt, und ein Objekt materialisiert sich. Tatsächlich macht @Component die Klasse nur zum Kandidaten, und die echte Erstellung findet in der Instanz-Erstellungsphase des Kontextstarts statt. Verwechselt man das, entsteht eine seltsame Logik wie „Warum wird der Konstruktor nicht beim Kompilieren aufgerufen?" Spoiler: Weil die Kompilierung den Container nicht startet.
Fehler Nr. 3: Alles mit @Component markieren, einschließlich Domain-Objekte.
Es gibt eine Versuchung: „Wenn Scanning bequem ist, soll Spring alles verwalten: Order, Customer, CreateOrderCommand…" Das verwässert schnell die Architektur und verwandelt den Container in eine Entität, die zu viel weiß. Domain-Objekte brauchen normalerweise kein Leben im Container: Sie werden für jede Operation erstellt, tragen Daten, und lassen sich mit einem normalen new leichter kontrollieren. In den Container kommen normalerweise Services und Infrastruktur – das, was lange lebt und einheitlich sein soll.
Fehler Nr. 4: Falsche Scan-Grenze setzen und dann Symptome statt Ursachen behandeln.
Ist basePackages zu eng, werden manche Komponenten nicht gefunden. Ist es zu weit, kann Unerwünschtes auftauchen, und dann beginnen unerwartete Konflikte. Ein Einsteiger macht es oft umgekehrt: Sieht einen Fehler beim Erstellen eines Services und beginnt, den Service zu korrigieren, obwohl das Problem darin liegt, dass die benötigte Komponente gar nicht registriert wurde. Die richtige Diagnose beginnt mit der Frage: „Ist die Klasse sicher im Suchbereich?"
Fehler Nr. 5: Vollständig auf den Standard-Bean-Namen vertrauen, wenn der Name wichtig ist.
In Lernbeispielen macht man manchmal einen Lookup nach Name, und dann wird ein Fehler im Namen zu Zeitverlust. Zum Beispiel erwartet man "orderPrinter", aber Spring hat einen anderen Namen generiert oder man hat einfach die Schreibweise falsch. Wenn man schon weiß, dass ein Name als Teil eines Vertrags dienen wird – z.B. in einer Demo –, sollte man ihn explizit über @Component("orderPrinter") setzen. Das macht das Verhalten vorhersehbar und spart Nerven.
GO TO FULL VERSION