Zirkuläre Abhängigkeiten im Frühjahr

1. Was ist eine zirkuläre Abhängigkeit?

Es passiert, wenn eine Bohne A von einer anderen Bohne B abhängt und die Bohne B auch von der Bohne A abhängt:

Bohne A → Bohne B → Bohne A.

Natürlich könnten wir mehr Bohnen haben:

Bohne A → Bohne B → Bohne C → Bohne D → Bohne E → Bohne A.

2. Was passiert im Frühling?

Wenn der Spring-Kontext alle Beans lädt, wird versucht, Beans in der Reihenfolge zu erstellen, in der sie vollständig funktionieren. Zum Beispiel, wenn wir keine zirkuläre Abhängigkeit hatten, wie im folgenden Fall:

Bohne A → Bohne B → Bohne C.

Der Frühling erzeugt Bohne C, dann Bohne B (und injiziert Bohne C hinein), dann Bohne A (und injiziert Bohne B hinein).

Bei einer zirkulären Abhängigkeit kann Spring jedoch nicht entscheiden, welche der Beans zuerst erstellt werden sollen, da sie voneinander abhängen. In diesen Fällen löst Spring beim Laden des Kontexts eine BeanCurrentlyInCreationException aus .

Dies kann im Frühjahr bei Verwendung der Konstruktorinjektion passieren . Wenn Sie andere Arten von Injektionen verwenden, sollten Sie dieses Problem nicht finden, da die Abhängigkeiten bei Bedarf injiziert werden und nicht beim Laden des Kontexts.

3. Ein kurzes Beispiel

Definieren wir zwei voneinander abhängige Beans (über Konstruktorinjektion):

@Component public class CircularDependencyA { private CircularDependencyB circB; @Autowired public CircularDependencyA(CircularDependencyB circB) { this.circB = circB; } }
@Component public class CircularDependencyB { private CircularDependencyA circA; @Autowired public CircularDependencyB(CircularDependencyA circA) { this.circA = circA; } }

Jetzt können wir eine Konfigurationsklasse für die Tests schreiben. Nennen wir sie TestConfig , die das Basispaket angibt, nach dem nach Komponenten gesucht werden soll. Nehmen wir an, unsere Beans sind im Paket „ com.baeldung.circulardependency “ definiert:

@Configuration @ComponentScan(basePackages = { "com.baeldung.circulardependency" }) public class TestConfig { }

Und schließlich können wir einen JUnit-Test schreiben, um die zirkuläre Abhängigkeit zu überprüfen. Der Test kann leer sein, da die zirkuläre Abhängigkeit beim Laden des Kontexts erkannt wird.

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { TestConfig.class }) public class CircularDependencyTest { @Test public void givenCircularDependency_whenConstructorInjection_thenItFails() { // Empty test; we just want the context to load } }

Wenn Sie versuchen, diesen Test auszuführen, wird die folgende Ausnahme angezeigt:

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA': Requested bean is currently in creation: Is there an unresolvable circular reference?

4. Die Problemumgehungen

Wir werden einige der beliebtesten Möglichkeiten zeigen, um mit diesem Problem umzugehen.

4.1. Neugestaltung

Wenn Sie eine zirkuläre Abhängigkeit haben, haben Sie wahrscheinlich ein Entwurfsproblem und die Verantwortlichkeiten sind nicht gut getrennt. Sie sollten versuchen, die Komponenten ordnungsgemäß neu zu gestalten, damit ihre Hierarchie gut gestaltet ist und keine zirkulären Abhängigkeiten erforderlich sind.

Wenn Sie die Komponenten nicht neu entwerfen können (es kann viele mögliche Gründe dafür geben: Legacy-Code, Code, der bereits getestet wurde und nicht geändert werden kann, nicht genügend Zeit oder Ressourcen für eine vollständige Neugestaltung…), gibt es einige Problemumgehungen, die Sie ausprobieren sollten.

4.2. Verwenden Sie @Lazy

Eine einfache Möglichkeit, den Zyklus zu unterbrechen, besteht darin, Spring zu sagen, um eine der Bohnen träge zu initialisieren. Das heißt: Anstatt die Bean vollständig zu initialisieren, wird ein Proxy erstellt, um sie in die andere Bean zu injizieren. Die injizierte Bohne wird erst dann vollständig erstellt, wenn sie zum ersten Mal benötigt wird.

Um dies mit unserem Code zu versuchen, können Sie die CircularDependencyA wie folgt ändern:

@Component public class CircularDependencyA { private CircularDependencyB circB; @Autowired public CircularDependencyA(@Lazy CircularDependencyB circB) { this.circB = circB; } }

Wenn Sie den Test jetzt ausführen, werden Sie feststellen, dass der Fehler diesmal nicht auftritt.

4.3. Verwenden Sie Setter / Field Injection

Eine der beliebtesten Problemumgehungen und auch die Vorschläge der Spring-Dokumentation ist die Verwendung der Setter-Injektion.

Einfach ausgedrückt, wenn Sie die Art und Weise ändern, in der Ihre Bohnen so verdrahtet sind, dass anstelle der Konstruktorinjektion eine Setter-Injektion (oder Feldinjektion) verwendet wird, ist das Problem behoben. Auf diese Weise erstellt Spring die Beans, aber die Abhängigkeiten werden erst injiziert, wenn sie benötigt werden.

Lassen Sie uns das tun - ändern wir unsere Klassen, um Setter-Injektionen zu verwenden, und fügen CircularDependencyB ein weiteres Feld ( Nachricht ) hinzu, damit wir einen ordnungsgemäßen Komponententest durchführen können:

@Component public class CircularDependencyA { private CircularDependencyB circB; @Autowired public void setCircB(CircularDependencyB circB) { this.circB = circB; } public CircularDependencyB getCircB() { return circB; } }
@Component public class CircularDependencyB { private CircularDependencyA circA; private String message = "Hi!"; @Autowired public void setCircA(CircularDependencyA circA) { this.circA = circA; } public String getMessage() { return message; } }

Jetzt müssen wir einige Änderungen an unserem Komponententest vornehmen:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { TestConfig.class }) public class CircularDependencyTest { @Autowired ApplicationContext context; @Bean public CircularDependencyA getCircularDependencyA() { return new CircularDependencyA(); } @Bean public CircularDependencyB getCircularDependencyB() { return new CircularDependencyB(); } @Test public void givenCircularDependency_whenSetterInjection_thenItWorks() { CircularDependencyA circA = context.getBean(CircularDependencyA.class); Assert.assertEquals("Hi!", circA.getCircB().getMessage()); } }

Im Folgenden werden die oben gezeigten Anmerkungen erläutert:

@Bean : Um Spring Framework mitzuteilen, dass diese Methoden verwendet werden müssen, um eine Implementierung der zu injizierenden Beans abzurufen.

@Test : Der Test ruft die CircularDependencyA-Bean aus dem Kontext ab und bestätigt, dass CircularDependencyB ordnungsgemäß injiziert wurde, und überprüft den Wert seiner Nachrichteneigenschaft .

4.4. Verwenden Sie @PostConstruct

Eine andere Möglichkeit, den Zyklus zu unterbrechen, besteht darin, eine Abhängigkeit mit @Autowired in eine der Beans einzufügen und anschließend die mit @PostConstruct kommentierte Methode zu verwenden , um die andere Abhängigkeit festzulegen .

Unsere Bohnen könnten den folgenden Code haben:

@Component public class CircularDependencyA { @Autowired private CircularDependencyB circB; @PostConstruct public void init() { circB.setCircA(this); } public CircularDependencyB getCircB() { return circB; } }
@Component public class CircularDependencyB { private CircularDependencyA circA; private String message = "Hi!"; public void setCircA(CircularDependencyA circA) { this.circA = circA; } public String getMessage() { return message; } }

Und wir können denselben Test ausführen, den wir zuvor hatten, sodass wir überprüfen, ob die zirkuläre Abhängigkeitsausnahme immer noch nicht ausgelöst wird und ob die Abhängigkeiten ordnungsgemäß eingefügt wurden.

4.5. Implementieren Sie ApplicationContextAware und InitializingBean

Wenn eine der Beans ApplicationContextAware implementiert , hat die Bean Zugriff auf den Spring-Kontext und kann die andere Bean von dort extrahieren. Bei der Implementierung von InitializingBean geben wir an, dass diese Bean einige Aktionen ausführen muss, nachdem alle ihre Eigenschaften festgelegt wurden. In diesem Fall möchten wir unsere Abhängigkeit manuell festlegen.

Der Code unserer Bohnen wäre:

@Component public class CircularDependencyA implements ApplicationContextAware, InitializingBean { private CircularDependencyB circB; private ApplicationContext context; public CircularDependencyB getCircB() { return circB; } @Override public void afterPropertiesSet() throws Exception { circB = context.getBean(CircularDependencyB.class); } @Override public void setApplicationContext(final ApplicationContext ctx) throws BeansException { context = ctx; } }
@Component public class CircularDependencyB { private CircularDependencyA circA; private String message = "Hi!"; @Autowired public void setCircA(CircularDependencyA circA) { this.circA = circA; } public String getMessage() { return message; } }

Wieder können wir den vorherigen Test ausführen und feststellen, dass die Ausnahme nicht ausgelöst wird und der Test wie erwartet funktioniert.

5. Abschließend

Es gibt viele Möglichkeiten, mit zirkulären Abhängigkeiten im Frühjahr umzugehen. Als Erstes müssen Sie Ihre Beans neu gestalten, damit keine zirkulären Abhängigkeiten erforderlich sind: Sie sind normalerweise ein Symptom für ein Design, das verbessert werden kann.

Wenn Sie jedoch unbedingt zirkuläre Abhängigkeiten in Ihrem Projekt benötigen, können Sie einige der hier vorgeschlagenen Problemumgehungen befolgen.

Die bevorzugte Methode ist die Verwendung von Setter-Injektionen. Es gibt jedoch auch andere Alternativen, die im Allgemeinen darauf beruhen, Spring daran zu hindern, die Initialisierung und Injektion der Bohnen zu verwalten, und dies selbst mit der einen oder anderen Strategie zu tun.

Die Beispiele finden Sie im GitHub-Projekt.