Dipendenze circolari in primavera

1. Che cos'è una dipendenza circolare?

Succede quando un fagiolo A dipende da un altro fagiolo B e anche il fagiolo B dipende dal fagiolo A:

Fagiolo A → Fagiolo B → Fagiolo A

Ovviamente, potremmo avere più fagioli impliciti:

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

2. Cosa succede in primavera

Quando il contesto Spring carica tutti i bean, cerca di creare i bean nell'ordine necessario affinché funzionino completamente. Ad esempio, se non avessimo una dipendenza circolare, come il seguente caso:

Fagiolo A → Fagiolo B → Fagiolo C

La primavera creerà il fagiolo C, quindi creerà il fagiolo B (e vi inietterà il fagiolo C), quindi creerà il fagiolo A (e vi inietterà il fagiolo B).

Ma, quando si ha una dipendenza circolare, Spring non può decidere quale dei bean debba essere creato per primo, poiché dipendono l'uno dall'altro. In questi casi, Spring genererà un'eccezione BeanCurrentlyInCreationException durante il caricamento del contesto.

Può accadere in primavera quando si utilizza l' iniezione del costruttore ; se usi altri tipi di iniezioni non dovresti riscontrare questo problema poiché le dipendenze verranno iniettate quando sono necessarie e non sul caricamento del contesto.

3. Un rapido esempio

Definiamo due bean che dipendono l'uno dall'altro (tramite iniezione del costruttore):

@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; } }

Ora possiamo scrivere una classe di configurazione per i test, chiamiamola TestConfig , che specifica il pacchetto di base per la scansione dei componenti. Supponiamo che i nostri bean siano definiti nel pacchetto " com.baeldung.circulardependency ":

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

E finalmente possiamo scrivere un test JUnit per verificare la dipendenza circolare. Il test può essere vuoto, poiché la dipendenza circolare verrà rilevata durante il caricamento del contesto.

@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 } }

Se provi a eseguire questo test, otterrai la seguente eccezione:

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

4. Le soluzioni alternative

Mostreremo alcuni dei modi più popolari per affrontare questo problema.

4.1. Riprogettare

Quando hai una dipendenza circolare, è probabile che tu abbia un problema di progettazione e le responsabilità non siano ben separate. Dovresti provare a riprogettare correttamente i componenti in modo che la loro gerarchia sia ben progettata e non siano necessarie dipendenze circolari.

Se non è possibile riprogettare i componenti (le ragioni possono essere molte: codice legacy, codice già testato e non modificabile, tempo o risorse insufficienti per una riprogettazione completa ...), ci sono alcune soluzioni alternative da provare.

4.2. Usa @Lazy

Un modo semplice per interrompere il ciclo è dire Spring per inizializzare pigramente uno dei fagioli. Cioè: invece di inizializzare completamente il bean, creerà un proxy per iniettarlo nell'altro bean. Il bean iniettato verrà creato completamente solo quando è necessario per la prima volta.

Per provare questo con il nostro codice, puoi modificare CircularDependencyA come segue:

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

Se esegui il test ora, vedrai che l'errore non si verifica questa volta.

4.3. Usa Setter / Field Injection

Una delle soluzioni alternative più popolari, e anche ciò che propone la documentazione di Spring, è l'utilizzo di setter injection.

In poche parole, se cambi il modo in cui i tuoi bean sono cablati per utilizzare l'iniezione setter (o l'iniezione di campo) invece dell'iniezione del costruttore, questo risolve il problema. In questo modo Spring crea i bean, ma le dipendenze non vengono iniettate finché non sono necessarie.

Facciamolo: cambiamo le nostre classi per utilizzare le iniezioni di setter e aggiungeremo un altro campo ( messaggio ) a CircularDependencyB in modo da poter eseguire un test unitario appropriato:

@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; } }

Ora dobbiamo apportare alcune modifiche al nostro unit test:

@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()); } }

Quanto segue spiega le annotazioni viste sopra:

@Bean : per dire al framework Spring che questi metodi devono essere usati per recuperare un'implementazione dei bean da iniettare.

@Test : il test otterrà il bean CircularDependencyA dal contesto e asserirà che il suo CircularDependencyB è stato iniettato correttamente, controllando il valore della sua proprietà del messaggio .

4.4. Usa @PostConstruct

Un altro modo per interrompere il ciclo è iniettare una dipendenza utilizzando @Autowired su uno dei bean, quindi utilizzare un metodo annotato con @PostConstruct per impostare l'altra dipendenza.

I nostri fagioli potrebbero avere il seguente codice:

@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; } }

E possiamo eseguire lo stesso test che avevamo in precedenza, quindi controlliamo che l'eccezione di dipendenza circolare non venga ancora lanciata e che le dipendenze siano correttamente inserite.

4.5. Implementa ApplicationContextAware e InitializingBean

Se uno dei bean implementa ApplicationContextAware , il bean ha accesso al contesto Spring e può estrarre l'altro bean da lì. Implementando InitializingBean indichiamo che questo bean deve eseguire alcune azioni dopo che tutte le sue proprietà sono state impostate; in questo caso, vogliamo impostare manualmente la nostra dipendenza.

Il codice dei nostri fagioli sarebbe:

@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; } }

Di nuovo, possiamo eseguire il test precedente e vedere che l'eccezione non viene generata e che il test funziona come previsto.

5. In conclusione

Esistono molti modi per gestire le dipendenze circolari in primavera. La prima cosa da considerare è ridisegnare i propri bean in modo che non siano necessarie dipendenze circolari: di solito sono un sintomo di un design che può essere migliorato.

Ma se hai assolutamente bisogno di avere dipendenze circolari nel tuo progetto, puoi seguire alcune delle soluzioni alternative suggerite qui.

Il metodo preferito sta usando le iniezioni di setter. Ma ci sono altre alternative, generalmente basate sull'impedire a Spring di gestire l'inizializzazione e l'iniezione dei bean, e farlo tu stesso usando una strategia o un'altra.

Gli esempi possono essere trovati nel progetto GitHub.