Circulaire afhankelijkheden in het voorjaar

1. Wat is een circulaire afhankelijkheid?

Het gebeurt wanneer boon A afhankelijk is van een andere boon B, en boon B ook afhankelijk is van boon A:

Bean A → Bean B → Bean A

Natuurlijk kunnen we meer bonen hebben geïmpliceerd:

Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

2. Wat gebeurt er in het voorjaar

Wanneer Spring Context alle bonen laadt, probeert het bonen te maken in de volgorde die nodig is om ze volledig te laten werken. Als we bijvoorbeeld geen circulaire afhankelijkheid hadden, zoals in het volgende geval:

Bean A → Bean B → Bean C

De lente zal boon C maken, dan boon B maken (en boon C erin injecteren), en dan boon A maken (en er boon B in injecteren).

Maar als Spring een cirkelvormige afhankelijkheid heeft, kan hij niet beslissen welke van de bonen als eerste moet worden gemaakt, omdat ze van elkaar afhankelijk zijn. In deze gevallen zal Spring een BeanCurrentlyInCreationException tijdens het laden van context.

Het kan in het voorjaar gebeuren bij gebruik constructor injectie; als u andere soorten injecties gebruikt, zou u dit probleem niet moeten vinden, aangezien de afhankelijkheden zullen worden geïnjecteerd wanneer ze nodig zijn en niet bij het laden van de context.

3. Een snel voorbeeld

Laten we twee bonen definiëren die van elkaar afhankelijk zijn (via constructor-injectie):

@Component openbare klasse CircularDependencyA {privé CircularDependencyB circB; @Autowired openbare CircularDependencyA (CircularDependencyB circB) {this.circB = circB; }}
@Component openbare klasse CircularDependencyB {privé CircularDependencyA circA; @Autowired openbare CircularDependencyB (CircularDependencyA circA) {this.circA = circA; }}

Nu kunnen we een configuratieklasse schrijven voor de tests, laten we het noemen TestConfig, dat het basispakket specificeert om te scannen op componenten. Laten we aannemen dat onze bonen zijn gedefinieerd in het pakket "com.baeldung.circulardependency”:

@Configuration @ComponentScan (basePackages = {"com.baeldung.circulardependency"}) openbare klasse TestConfig {}

En tot slot kunnen we een JUnit-test schrijven om de circulaire afhankelijkheid te controleren. De test kan leeg zijn, aangezien de circulaire afhankelijkheid wordt gedetecteerd tijdens het laden van de context.

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = {TestConfig.class}) openbare klasse CircularDependencyTest {@Test openbare leegte gegevenCircularDependency_whenConstructorInjection_thenItFails () {// Lege test; we willen gewoon dat de context wordt geladen}}

Als u deze test probeert uit te voeren, krijgt u de volgende uitzondering:

BeanCurrentlyInCreationException: fout bij het maken van een bean met de naam 'circularDependencyA': de aangevraagde bean wordt momenteel gemaakt: is er een onoplosbare kringverwijzing?

4. De tijdelijke oplossingen

We zullen enkele van de meest populaire manieren laten zien om met dit probleem om te gaan.

4.1. Herontwerp

Als je een circulaire afhankelijkheid hebt, heb je waarschijnlijk een ontwerpprobleem en zijn de verantwoordelijkheden niet goed gescheiden. U moet proberen de componenten op de juiste manier opnieuw te ontwerpen, zodat hun hiërarchie goed is ontworpen en er geen behoefte is aan circulaire afhankelijkheden.

Als u de componenten niet opnieuw kunt ontwerpen (er kunnen veel mogelijke redenen zijn: verouderde code, code die al is getest en niet kan worden gewijzigd, niet genoeg tijd of middelen voor een volledig herontwerp ...), zijn er enkele oplossingen om te proberen.

4.2. Gebruik @Lui

Een eenvoudige manier om de cyclus te doorbreken, is door Lente te zeggen om een ​​van de bonen lui te initialiseren. Dat wil zeggen: in plaats van de bean volledig te initialiseren, zal het een proxy creëren om deze in de andere bean te injecteren. De geïnjecteerde boon wordt pas volledig gemaakt wanneer deze voor het eerst nodig is.

Om dit met onze code te proberen, kunt u de CircularDependencyA wijzigen in het volgende:

@Component openbare klasse CircularDependencyA {privé CircularDependencyB circB; @Autowired openbare CircularDependencyA (@Lazy CircularDependencyB circB) {this.circB = circB; }}

Als u de test nu uitvoert, zult u zien dat de fout deze keer niet optreedt.

4.3. Gebruik Setter / Field Injection

Een van de meest populaire oplossingen, en ook wat Spring-documentatie voorstelt, is het gebruik van setter-injectie.

Simpel gezegd, als u de manier waarop uw bonen zijn bedraad verandert om setterinjectie (of veldinjectie) te gebruiken in plaats van constructorinjectie, wordt het probleem opgelost. Op deze manier creëert Spring de bonen, maar de afhankelijkheden worden pas geïnjecteerd als ze nodig zijn.

Laten we dat doen - laten we onze klassen veranderen om setter-injecties te gebruiken en een ander veld toevoegen (bericht) naar Circulaire afhankelijkheid B. zodat we een goede unit-test kunnen maken:

@Component openbare klasse CircularDependencyA {privé CircularDependencyB circB; @Autowired public void setCircB (CircularDependencyB circB) {this.circB = circB; } openbare CircularDependencyB getCircB () {return circB; }}
@Component openbare klasse CircularDependencyB {privé CircularDependencyA circA; private String message = "Hallo!"; @Autowired openbare leegte setCircA (CircularDependencyA circA) {this.circA = circA; } public String getMessage () {retourbericht; }}

Nu moeten we enkele wijzigingen aanbrengen in onze unit-test:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = {TestConfig.class}) openbare klasse CircularDependencyTest {@Autowired ApplicationContext context; @Bean openbare CircularDependencyA getCircularDependencyA () {terugkeer nieuwe CircularDependencyA (); } @Bean openbare CircularDependencyB getCircularDependencyB () {retourneer nieuwe CircularDependencyB (); } @Test openbare leegte gegevenCircularDependency_whenSetterInjection_thenItWorks () {CircularDependencyA circA = context.getBean (CircularDependencyA.class); Assert.assertEquals ("Hallo!", CircA.getCircB (). GetMessage ()); }}

Het volgende verklaart de bovenstaande annotaties:

@Boon: Om het Spring-raamwerk te vertellen dat deze methoden moeten worden gebruikt om een ​​implementatie van de te injecteren bonen op te halen.

@Test: De test haalt CircularDependencyA bean uit de context en beweert dat zijn CircularDependencyB correct is geïnjecteerd, waarbij de waarde van zijn bericht eigendom.

4.4. Gebruik @PostConstruct

Een andere manier om de cyclus te doorbreken is het injecteren van een afhankelijkheid met @Autowired op een van de bonen en gebruik vervolgens een methode die is geannoteerd met @PostConstruct om de andere afhankelijkheid in te stellen.

Onze bonen kunnen de volgende code hebben:

@Component openbare klasse CircularDependencyA {@Autowired privé CircularDependencyB circB; @PostConstruct public void init () {circB.setCircA (dit); } openbare CircularDependencyB getCircB () {return circB; }}
@Component openbare klasse CircularDependencyB {privé CircularDependencyA circA; private String message = "Hallo!"; openbare ongeldige setCircA (CircularDependencyA circA) {this.circA = circA; } public String getMessage () {retourbericht; }}

En we kunnen dezelfde test uitvoeren die we eerder hadden, dus we controleren of de circulaire afhankelijkheidsuitzondering nog steeds niet wordt gegenereerd en of de afhankelijkheden correct zijn geïnjecteerd.

4.5. Implementeren ApplicationContextAware en Initialiseren Bean

Als een van de bonen werkt ApplicationContextAwareheeft de boon toegang tot Spring-context en kan de andere boon daaruit halen. Implementeren Initialiseren Bean we geven aan dat deze boon een aantal acties moet uitvoeren nadat alle eigenschappen zijn ingesteld; in dit geval willen we onze afhankelijkheid handmatig instellen.

De code van onze bonen zou zijn:

@Component openbare klasse CircularDependencyA implementeert ApplicationContextAware, InitializingBean {private CircularDependencyB circB; privé ApplicationContext-context; openbare CircularDependencyB getCircB () {retour circB; } @Override public void afterPropertiesSet () gooit uitzondering {circB = context.getBean (CircularDependencyB.class); } @Override public void setApplicationContext (laatste ApplicationContext ctx) gooit BeansException {context = ctx; }}
@Component openbare klasse CircularDependencyB {privé CircularDependencyA circA; private String message = "Hallo!"; @Autowired openbare leegte setCircA (CircularDependencyA circA) {this.circA = circA; } public String getMessage () {retourbericht; }}

Nogmaals, we kunnen de vorige test uitvoeren en zien dat de uitzondering niet wordt gegenereerd en dat de test werkt zoals verwacht.

5. Tot slot

Er zijn veel manieren om in het voorjaar met circulaire afhankelijkheden om te gaan. Het eerste dat u moet overwegen, is om uw bonen opnieuw te ontwerpen, zodat er geen circulaire afhankelijkheden nodig zijn: ze zijn meestal een symptoom van een ontwerp dat kan worden verbeterd.

Maar als u absoluut circulaire afhankelijkheden in uw project nodig heeft, kunt u enkele van de hier voorgestelde oplossingen volgen.

De voorkeursmethode is het gebruik van setter-injecties. Maar er zijn andere alternatieven, meestal gebaseerd op voorkomen dat Spring de initialisatie en injectie van de bonen beheert en dat zelf doet met behulp van een of andere strategie.

De voorbeelden zijn te vinden in het GitHub-project.