CodeGym /Kurse /Spring Core /Fehler bei der Bean-Suche in Spring

Fehler bei der Bean-Suche in Spring

Spring Core
Level 4, Lektion 4
Verfügbar

1. Stack Trace als Sprache des Containers

Der Stack Trace ist die Sprache, in der der Container erklärt, warum er die Applikation nicht zusammenbauen konnte. Wir haben bereits gesehen, dass er noch während refresh() abstürzt, wenn er in der Erstellungsphase den Abhängigkeitsgraphen nicht zusammenbauen kann. Jetzt ist es wichtig zu lernen, diesen Absturz zu lesen, statt nur auf den roten Stack Trace auf einem halben Bildschirm zu starren.

Wenn man das erste Mal einen langen Stack Trace auf einem halben Bildschirm sieht, versucht das Gehirn ehrlich zu tun, als wäre nichts passiert, und schlägt vor, die Applikation „auf gut Glück" neu zu starten. Aber Spring meckert in solchen Momenten nicht und „ruiniert dir das Leben" nicht – er tut genau das, was er soll: Er sagt, dass sich der Objektgraph nicht zusammenbauen lässt. Hätte der Container geschwiegen und sich „irgendwie gestartet", würde man ein kaputtes Programm bekommen, das nicht beim Start abstürzt, sondern mitten im Szenario – wenn der Nutzer schon denkt, dass alles funktioniert.

Wichtig ist, die Einstellung umzustellen: Ein Start-Fehler ist keine Katastrophe, sondern ein Diagnosebericht. Der Container zeigt, welche Bean er zu erstellen versuchte, welche Abhängigkeit er einsetzen wollte und warum er es nicht konnte. Unsere Aufgabe ist es, diesen Bericht aus der Exception-Sprache ins Menschliche zu übersetzen: „Bean X ist nicht registriert", „es wurden zwei Kandidaten statt einem gefunden", „Bean A wurde nicht zusammengebaut, weil Bean B nicht zusammengebaut wurde…". Nach ein paar solchen Analysen hört Spring auf, eine „Black Box" zu sein, und wird zum sehr gesprächigen Kollegen.

Drei Grundsituationen

Fast alle ersten Container-Abstürze bei Einsteigern lassen sich in drei Szenarien einteilen. Das erleichtert am Anfang enorm: Man sieht eine Exception – und weiß ungefähr, in welche Richtung man graben muss. Halten wir das in einer kompakten Tabelle fest, damit im Kopf eine „Landkarte" entsteht und nicht das Gefühl eines endlosen Fehler-Waldes.

Situation Wie es in Spring aussieht Was das menschlich bedeutet
Die benötigte Bean ist nicht vorhanden NoSuchBeanDefinitionException „Du fragst nach AuditWriter, aber ich weiß nicht, wo ich ihn herbekommen soll – du hast ihn nicht registriert."
Es gibt mehrere passende Beans NoUniqueBeanDefinitionException „Du fragst nach NotificationSender, aber ich habe zwei oder drei davon. Ich bin kein Hellseher und rate nicht."
Die Bean wurde nicht zusammengebaut, weil eine Abhängigkeit kaputt ist UnsatisfiedDependencyException „Ich habe versucht, ScenarioRunner zu erstellen, aber er hängt von OrderPlacementService ab, der wiederum von AuditWriter abhängt, der nicht vorhanden ist oder zu oft vorkommt."

Wichtige Nuance: UnsatisfiedDependencyException klingt oft am beängstigendsten, weil es sich anhört wie „etwas ist nicht befriedigt, alles ist schlecht". Aber meistens ist das nur ein Wrapper, der sagt: „Ich konnte diese Bean nicht erstellen, weil es in ihren Abhängigkeiten ein echtes Problem gibt." Die echte Ursache steckt meistens in einem der Caused by-Einträge.

Wo das Problem genau auftaucht

Dasselbe Grundproblem kann an zwei Stellen auftreten:

– beim Lookup – der Kontext ist bereits gestartet, und du hast selbst context.getBean(...) aufgerufen und etwas angefragt, das der Container nicht liefern kann;
– beim Kontextstart – der Container ist auf dasselbe Problem während refresh() gestoßen, als er eine andere Bean erstellte.

Im zweiten Fall siehst du oben öfter UnsatisfiedDependencyException, weil nicht dein expliziter Lookup bricht, sondern das Zusammenbauen einer abhängigen Bean. Aber die Wurzel ist dieselbe: Der Container konnte das benötigte Objekt nicht liefern – entweder ist es nicht vorhanden, oder es gibt zu viele passende Kandidaten.

2. Container hat die benötigte Bean nicht gefunden

Zunächst schauen wir auf den direktesten Fall: Der Kontext ist bereits gestartet, und du fragst selbst nach einer Bean, die dort nicht existiert. Dieser Fehler ist wie eine Mitteilung vom Lageristen: Du kommst nach einer Kiste mit der Aufschrift AuditWriter, er öffnet das Lager und sagt: „So eine Kiste habe ich nicht. Vielleicht hast du vergessen, sie zu liefern." In Spring bedeutet das, dass der Container die Bean nach deiner Anfrage nicht gefunden hat – weder nach Typ noch nach Name. Die gute Nachricht: Das ist einer der „ehrlichsten" Fehler – er sagt normalerweise direkt, was nicht gefunden wurde.

Fangen wir mit dem einfachsten Fall an: Du versuchst, eine Bean nach Typ zu holen, hast sie aber nicht registriert.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Bean nach Typ anfragen: Falls im Kontext keine Bean Definition für AuditWriter vorhanden ist – stürzen wir hier ab
    context.getBean(AuditWriter.class); // Wenn keine Bean vorhanden – NoSuchBeanDefinitionException
}

Hier ist wichtig zu verstehen: Der Container sucht nicht nach einer „Klasse irgendwo im Projekt", sondern nach einer Bean Definition innerhalb des Kontexts. Wenn in AppConfig kein AuditWriter registriert wurde, hat der Container einfach nichts zurückzugeben.

Wenn dieselbe fehlende Bean nicht direkt von dir, sondern von einer anderen Bean während refresh() benötigt wird, ist die Fehlermeldung länger: Oben erscheint oft UnsatisfiedDependencyException, aber im Kern bleibt dieselbe NoSuchBeanDefinitionException. Diese Kette analysieren wir etwas weiter unten am Beispiel eines Start-Absturzes.

Die zweite häufige Variante: Suche nach Name. Besonders schmerzhaft, wenn man sicher ist, dass „der Name doch genau so ist", aber sich ein Tippfehler oder ein anderer Naming-Stil eingeschlichen hat.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Suche nach Name: ein Tippfehler – und der Container glaubt, die Bean existiert nicht
    context.getBean("auditWritter"); // Tippfehler: Writter statt Writer
}

In solchen Fällen sagt die Exception-Meldung normalerweise direkt so etwas wie „No bean named auditWritter available". Und das ist genau der seltene Moment, in dem den Fehlertext zu lesen keine leere Formalität ist, sondern der kürzeste Weg zur Wahrheit.

Noch eine Variante, die überraschend häufig auftaucht: Du hast eine Bean registriert, fragst aber nach dem falschen Typ. Im Kontext gibt es z.B. ConsoleAuditWriter, du fragst aber nach FileAuditWriter. Aus Sicht des Containers ist das eine normale Situation: Du fragst nach einem bestimmten Typ, den er nicht kennt.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Im Kontext gibt es möglicherweise ConsoleAuditWriter, aber wenn wir FileAuditWriter anfragen – ist das ein anderer Typ
    context.getBean(FileAuditWriter.class); // du hast aber nur ConsoleAuditWriter
}

Hier hilft eine einfache Regel: Suche nach Typ ist eine Suche nach einem kompatiblen Typ. Wenn im Kontext eine Bean vorhanden ist, die einer Variablen des benötigten Typs zugewiesen werden kann – z.B. eine Interface-Implementierung –, passt sie. Wenn nicht, sieht der Container, als existiere eine solche Bean nicht.

3. Container hat mehrere Kandidaten gefunden

Dieser Fehler erscheint meistens genau dann, wenn man etwas völlig Normales tut: eine zweite Interface-Implementierung hinzufügen. Für ContextFlow ist das sogar der natürliche Weg: Ein AuditWriter schreibt in die Konsole, ein anderer in eine Datei; ein NotificationSender schickt E-Mails, ein anderer SMS. Der Container hat nichts dagegen, aber er braucht, dass die Wahl eindeutig ist. Ist sie nicht eindeutig, gibt er einen Fehler – denn raten wird Spring nicht.

Schauen wir uns ein Minimalbeispiel an. Wir deklarieren zwei Beans desselben Interfaces und versuchen, eine nach Typ zu holen.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AmbiguousAppConfig {

    @Bean
    public AuditWriter consoleAuditWriter() {
        // Erster Kandidat für die Rolle AuditWriter
        return new ConsoleAuditWriter();
    }

    @Bean
    public AuditWriter fileAuditWriter() {
        // Zweiter Kandidat für die Rolle AuditWriter
        return new FileAuditWriter();
    }
}

Und jetzt dieser Code:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AmbiguousAppConfig.class)) {
    // Wir fragen nach "einem AuditWriter", aber der Container sieht zwei Kandidaten – also stürzt er ab
    context.getBean(AuditWriter.class); // NoUniqueBeanDefinitionException
}

Die Container-Logik ist einfach: Zum Typ AuditWriter passen zwei Beans. Aber getBean(AuditWriter.class) ist eine Anfrage nach „gib mir einen". Der Container kann nicht entscheiden, welcher der beiden „richtiger" ist, und schreibt daher einen Fehler wie „expected single matching bean but found 2: consoleAuditWriter, fileAuditWriter".

Ein sehr häufiger Einsteiger-Fehler in diesem Moment: Man hofft, dass alias das Problem löst. Aber alias ist nur ein zweiter Name für dieselbe Bean. Er verwandelt zwei Beans nicht in eine und hilft nicht dabei, die „Haupt"-Bean zu wählen. Wenn im Container zwei verschiedene Instanzen liegen, macht ein Alias die Situation nicht weniger mehrdeutig.

Für jetzt reicht es, das Symptom der Mehrdeutigkeit zu sehen. Aber wichtig ist, daraus keine falsche Regel zu machen: Mehrere Implementierungen eines Interfaces sind ein normaler Teil des Designs. Der Container braucht dann einfach eine explizite Auswahlmethode – über @Primary, @Qualifier, @Fallback oder Bean-Kollektions-Injektion.

Manchmal ist es für die Diagnose nützlich, eine Bean explizit nach Name abzufragen, um sicherzustellen, dass beide wirklich existieren und dass das Problem tatsächlich die Mehrdeutigkeit ist. Das sieht so aus:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AmbiguousAppConfig.class)) {
    // Bean explizit nach Name auswählen: damit kann man bestätigen, dass beide Beans wirklich registriert sind
    AuditWriter writer = context.getBean("consoleAuditWriter", AuditWriter.class);
    System.out.println(writer.getClass().getSimpleName()); // ConsoleAuditWriter
}

Beachte: Das „heilt" die Situation nur an der Stelle, wo du den Lookup machst. Wenn die Mehrdeutigkeit aber im Wiring liegt – etwa wenn eine Bean von AuditWriter abhängt –, stürzt der Kontext bereits beim Start ab, und bis zu deinem getBean("consoleAuditWriter", AuditWriter.class) kommt es gar nicht. Das ist der Sinn von fail-fast: Der Container will keine Applikation mit unklarem Wiring starten.

4. „Eine Bean ist abgestürzt, aber eine andere ist schuld"

Jetzt nehmen wir die Variante des Start-Absturzes: Du fragst nichts aus dem Kontext ab, aber refresh() kann nicht abgeschlossen werden, weil eine Bean eine kaputte Abhängigkeit mit sich zieht.

Auf den ersten Blick scheint diese Exception die schlimmste zu sein. Du startest die Applikation, sie stürzt ab, und da steht so etwas wie „Error creating bean with name 'scenarioRunner'" – und man möchte ScenarioRunner reparieren. Aber das ist oft eine falsche Fährte. UnsatisfiedDependencyException bedeutet: Der Container hat versucht, eine Bean zu erstellen, und konnte eine ihrer Abhängigkeiten nicht befriedigen. Das Problem liegt also meistens nicht in der Bean selbst, sondern darin, dass sie vom Container etwas verlangt, was der Container nicht liefern kann.

Stell dir die Kette in ContextFlow vor: ScenarioRunner hängt von OrderPlacementService ab, der von AuditWriter abhängt. Und AuditWriter haben wir vergessen zu registrieren. Dann „stürzt" ScenarioRunner ab, obwohl er selbst perfekt geschrieben sein kann.

Der Graph unten ist bewusst auf eine einzige Problemkante gekürzt: ScenarioRunner → OrderPlacementService → AuditWriter. Die übrigen Abhängigkeiten des Services lassen wir weg, weil jetzt die Absturzkette selbst wichtig ist.

Hier ein kleines Konfigurationsbeispiel mit fehlender Abhängigkeit:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BrokenAppConfig {

    @Bean
    public ScenarioRunner scenarioRunner(OrderPlacementService service) {
        // ScenarioRunner kann nicht zusammengebaut werden, wenn OrderPlacementService nicht zusammengebaut wurde
        return new ScenarioRunner(service);
    }

    @Bean
    public OrderPlacementService orderPlacementService(AuditWriter auditWriter) {
        // Die anderen Abhängigkeiten des Services lassen wir hier weg: wir wollen eine fehlende Kante sehen
        return new OrderPlacementService(auditWriter);
    }

    // AuditWriter Bean fehlt: hier liegt die Wurzel des Problems
}

Was passiert in der Realität? Der Kontext versucht, ScenarioRunner zu erstellen, sieht, dass er OrderPlacementService braucht, beginnt OrderPlacementService zu erstellen, sieht, dass er AuditWriter braucht – und hier bricht alles. Daher siehst du oben UnsatisfiedDependencyException für ScenarioRunner oder OrderPlacementService, und unten, näher an der Wurzel, steht NoSuchBeanDefinitionException für AuditWriter.

Schematisch sieht so eine Kette oft so aus:

// Beispiel: oben Wrapper (wer "abgestürzt" ist), unten die Problemwurzel (was fehlte)
UnsatisfiedDependencyException: Error creating bean 'scenarioRunner'
Caused by: UnsatisfiedDependencyException: Error creating bean 'orderPlacementService'
Caused by: NoSuchBeanDefinitionException: No qualifying bean of type 'AuditWriter' available

Hier ist die wichtigste Gewohnheit des Tages: Nicht bei der ersten, obersten Exception stehen bleiben. Die oberste Exception zeigt, welche Bean der Container im Moment des Absturzes zu erstellen versuchte. Aber die Ursache liegt fast immer tiefer.

5. Algorithmus und Diagnose von Start-Fehlern

Mini-Algorithmus zum Lesen eines Start-Fehlers

Wenn man jedes Mal den Stack Trace „auf Sicht" analysiert, ermüdet das Gehirn sehr schnell. Nützlicher ist ein kleiner mechanischer Algorithmus: Wie öffnet man den Stack Trace und versteht in einer Minute, ob das ein „keine Bean" oder ein „Bean nicht eindeutig" ist – und wo genau. Ein solcher Algorithmus ersetzt nicht das Verständnis, macht die Diagnose aber ruhiger: Man kämpft nicht gegen Chaos, sondern geht Schritt für Schritt.

Stell dir vor, die Exceptions sind eine Matroschka-Puppe. Die äußere, große Puppe ist UnsatisfiedDependencyException, innen steckt eine kleinere, und irgendwo ganz im Zentrum sitzt der eigentliche Schuldige: NoSuchBeanDefinitionException oder NoUniqueBeanDefinitionException. Wir suchen nicht die äußere Puppe, sondern die im Zentrum.

Unten ein einfaches Schema, wie ich das Einsteigern normalerweise erkläre.

flowchart TD
    A[Kontext startet nicht] --> B[Wir schauen auf die oberste Exception: welche Bean ist abgestürzt]
    B --> C[Wir blättern nach unten bis zum ersten Caused by]
    C --> D[Wiederholen: wir suchen das nächste Caused by]
    D --> E{Was ist ganz unten für ein Typ?}
    E -->|NoSuchBeanDefinitionException| F[Bean nicht gefunden: Registrierung, Name und Typ prüfen]
    E -->|NoUniqueBeanDefinitionException| G[Mehrere gefunden: Abhängigkeit eindeutig machen]
    E -->|Sonstiges| H[Nachricht lesen: Konstruktor-/Methodenparameter, Bean-Name, geforderter Typ]

In der Praxis sieht das so aus: Man sucht im Stack Trace die Caused by-Blöcke, bewegt sich zum untersten, wo nicht mehr „Error creating bean X" steht, sondern ein konkretes „No qualifying bean of type ..." oder „expected single matching bean but found ...". Dann geht man ein bisschen nach oben und schaut, wer genau diese Abhängigkeit angefordert hat: Welche Bean und welcher Parameter. Dort steht oft direkt „Unsatisfied dependency expressed through parameter 0" – das ist der Hinweis, welche Abhängigkeit nicht eingesetzt wurde.

Diagnose-Tricks: „Was sieht der Container überhaupt in sich?"

Wenn man den Fehlertyp schon verstanden hat – keine Bean oder zu viele Kandidaten –, möchte man manchmal schnell prüfen, ob der Container wirklich das registriert hat, was man denkt. Besonders nützlich ist das, wenn man sicher ist, dass AuditWriter „sicher vorhanden ist", Spring aber sagt, er ist es nicht. In so einer Situation ist es besser, nicht mit dem Container zu streiten – er ist stur –, sondern ihn zu fragen: „Okay, welche AuditWriter siehst du überhaupt?"

Im ApplicationContext gibt es Methoden, die sich für solche Diagnosen hervorragend eignen. Wichtig ist, daran zu denken: Das ist kein alltäglicher Business-Code-Stil, sondern eine temporäre Taschenlampe zum Debuggen des Wirings.

import org.springframework.context.ApplicationContext;

import java.util.Arrays;

public class ContextDiagnostics {

    public static void printAuditWriters(ApplicationContext context) {
        // Den Container nach den Bean-Namen fragen, die zum Typ AuditWriter passen
        String[] names = context.getBeanNamesForType(AuditWriter.class);

        // Wenn das Array leer ist – gibt es keinen einzigen Kandidaten; wenn mehrere Elemente – wird es eine Mehrdeutigkeit geben
        System.out.println(Arrays.toString(names)); // z.B.: [consoleAuditWriter]
    }
}

Wenn names leer ist, hat der Container wirklich keinen einzigen AuditWriter registriert. Dann geht man in die Konfiguration und sucht, wo er erscheinen soll. Wenn names zwei Namen enthält und man eine NoUniqueBeanDefinitionException hatte, lügt der Container nicht: Es gibt wirklich mehrere Kandidaten.

Ein ähnlicher Trick ist manchmal nützlich für die Diagnose eines „falschen Namens":

import org.springframework.context.ApplicationContext;

public class NameDiagnostics {

    public static void checkBean(ApplicationContext context, String beanName) {
        // Schnelle Prüfung: Gibt es im Container eine Bean mit diesem Namen?
        boolean exists = context.containsBean(beanName);

        // Nützlich, wenn man einen Tippfehler oder einen anderen Naming-Stil vermutet
        System.out.println(beanName + " exists = " + exists); // auditWriter exists = false
    }
}

Solche Prüfungen helfen dem Einsteiger, schnell in die Realität zurückzukommen: nicht raten, sondern schauen, was der Container sieht. Aber ich wiederhole den wichtigen Gedanken: Wenn sich solche Methoden über Business-Services verteilen, entsteht Service-Locator-ähnlicher Code, der schwieriger zu testen und zu warten ist. Daher die Diagnose am Start-Punkt oder an einer separaten Debug-Stelle halten und nach der Wiring-Reparatur entfernen.

6. Typische Fehler bei der Diagnose

Diagnose-Fehler sehen oft nicht wie „schlechter Java-Code" aus, sondern wie eine schlechte Gewohnheit beim Lesen von Container-Meldungen. Dadurch kann jemand stundenlang die falsche Klasse reparieren, mit der Exception streiten und wahllose getBean()-Aufrufe hinzufügen in der Hoffnung, „na, irgendwie wird's starten". Unten die beliebtesten Fallen, in die fast alle tappen – und das ist normal. Hauptsache, man erkennt sie rechtzeitig an ihren Spuren.

Fehler Nr. 1: Nur auf die oberste Zeile des Stack Trace schauen.
Die oberste Zeile sagt fast immer, welche Bean der Container zuletzt zu erstellen versucht hat. Das ist wie die Situation „die Decke ist eingestürzt, also ist die Decke schuld." In Wirklichkeit steckt die Ursache oft weiter unten, im Caused by, wo steht „AuditWriter nicht gefunden" oder „zwei NotificationSender gefunden". Die Gewohnheit, das unterste Caused by zu suchen, spart enorm viel Zeit.

Fehler Nr. 2: Die abgestürzte Bean reparieren statt die Abhängigkeit, wegen der sie nicht zusammengebaut wurde.
UnsatisfiedDependencyException verleitet oft dazu, ScenarioRunner zu reparieren, weil er in der Meldung auftaucht. Aber er ist meistens „abgestürzt", weil der Container seine Abhängigkeiten nicht zusammenbauen konnte. Daher muss man den Objektgraphen im Kopf behalten: ScenarioRunner → OrderPlacementService → AuditWriter. Und genau dort reparieren, wo die Kette bricht.

Fehler Nr. 3: „Keine Bean" und „zu viele Beans" verwechseln.
NoSuchBeanDefinitionException und NoUniqueBeanDefinitionException klingen ähnlich, haben aber entgegengesetzte Bedeutungen. Im ersten Fall sagt der Container „Gib mir wenigstens einen", und er hat null. Im zweiten – „Gib mir einen", und er hat zwei. Die Lösungen sind auch verschieden, und der Versuch, „noch eine Bean hinzuzufügen" bei NoUnique... macht es nur schlimmer.

Fehler Nr. 4: Denken, dass das Vorhandensein einer Klasse im Projekt eine Bean im Container bedeutet.
Einsteiger denken oft: „Ich habe doch ConsoleAuditWriter.java, warum sieht Spring es nicht?" Weil der Container keine Dateien im Projekt sieht, sondern registrierte Bean Definitions. Solange man dem Container nicht gesagt hat „Das hier ist eine Bean", bleibt es einfach eine Klasse auf der Festplatte.

Fehler Nr. 5: Versuchen, alles „schnell zu reparieren", indem man context.getBean(...) im Business-Code verstreut.
Nach ein paar Fehlern entsteht manchmal die Versuchung: „Na gut, ich hole mir die benötigte Bean im Service selbst nach Name." So kann die Applikation zwar starten, aber man verwandelt DI unmerklich in Service Locator, und die Abhängigkeiten werden wieder versteckt – nur jetzt verstecken sie sich in getBean() statt in new. Für das Lernprojekt und für normales architektonisches Wachstum ist das ein schlechter Weg.

1
Aufgabe
Spring Core, Level 4, Lektion 4
Gesperrt
Lookup-Fehler für fehlenden Bean
Lookup-Fehler für fehlenden Bean
1
Aufgabe
Spring Core, Level 4, Lektion 4
Gesperrt
Hauptursache für Startup-Fehler
Hauptursache für Startup-Fehler
1
Umfrage/Quiz
Spring Beans, Level 4, Lektion 4
Nicht verfügbar
Spring Beans
Beans und Context
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION