Kružne ovisnosti u proljeće
1. Što je kružna ovisnost?
To se događa kada grah A ovisi o drugom grahu B, a grah B ovisi i o grahu A:
Grah A → Grah B → Grah A
Naravno, mogli bismo podrazumijevati više graha:
Grah A → Grah B → Grah C → Grah D → Grah E → Grah A
2. Što se događa u proljeće
Kada kontekst Spring učitava sve grah, on pokušava stvoriti grah redoslijedom potrebnim za njihov potpuni rad. Na primjer, ako nismo imali kružnu ovisnost, kao u sljedećem slučaju:
Grah A → Grah B → Grah C
Proljeće će stvoriti zrno C, zatim stvoriti zrno B (i u njega ubrizgati zrno C), zatim stvoriti zrno A (i u njega ubrizgati zrno B).
Ali, kad ima kružnu ovisnost, Spring ne može odlučiti koji od graha treba stvoriti prvi, jer ovise jedan o drugome. U tim će slučajevima Proljeće podići a BeanCurrentlyInCreationException tijekom učitavanja konteksta.
To se može dogoditi u proljeće kada se koristi ubrizgavanje konstruktora; ako koristite druge vrste injekcija, ne biste trebali pronaći ovaj problem, jer će se ovisnosti ubrizgati kada su potrebne, a ne na kontekstualnom učitavanju.
3. Brzi primjer
Definirajmo dva zrna koja međusobno ovise (putem ubrizgavanja konstruktora):
@Component javna klasa CircularDependencyA {private CircularDependencyB circB; @Autowired javni CircularDependencyA (CircularDependencyB circB) {this.circB = circB; }}
@Component javna klasa CircularDependencyB {private CircularDependencyA circA; @Autowired javni CircularDependencyB (CircularDependencyA circA) {this.circA = circA; }}
Sada možemo napisati klasu konfiguracije za testove, nazovimo je TestConfig, koji navodi osnovni paket za skeniranje komponenata. Pretpostavimo da je naš grah definiran u paketu "com.baeldung.kružna ovisnost”:
@Configuration @ComponentScan (basePackages = {"com.baeldung.circulardependency"}) javna klasa TestConfig {}
I na kraju možemo napisati JUnit test za provjeru kružne ovisnosti. Test može biti prazan, jer će se kružna ovisnost otkriti tijekom kontekstnog učitavanja.
@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = {TestConfig.class}) public class CircularDependencyTest {@Test public void givenCircularDependency_whenConstructorInjection_thenItFails () {// Prazan test; samo želimo da se kontekst učita}}
Ako pokušate pokrenuti ovaj test, dobit ćete sljedeću iznimku:
BeanCurrentlyInCreationException: Pogreška pri stvaranju graha s imenom 'circularDependencyA': Traženi grah je trenutno u izradi: Postoji li nerješiva kružna referenca?
4. Zaobilazna rješenja
Pokazat ćemo neke od najpopularnijih načina rješavanja ovog problema.
4.1. Redizajn
Kada imate kružnu ovisnost, vjerojatno imate problema s dizajnom i odgovornosti nisu dobro razdvojene. Trebali biste pokušati pravilno redizajnirati komponente tako da je njihova hijerarhija dobro dizajnirana i nema potrebe za kružnim ovisnostima.
Ako ne možete redizajnirati komponente (za to može postojati mnogo mogućih razloga: naslijeđeni kôd, kôd koji je već testiran i koji se ne može izmijeniti, nema dovoljno vremena ili resursa za cjeloviti redizajn ...), pokušati ćete zaobići nekoliko rješenja.
4.2. Koristiti @Lijen
Jednostavan način da prekinete ciklus je izgovor da Proljeće lijeno inicijalizira jedan od graha. Odnosno: umjesto potpune inicijalizacije graha, stvorit će proxy za ubrizgavanje u drugi grah. Ubrizgani grah u potpunosti će se stvoriti tek kad je to prvo potrebno.
Da biste isprobali ovo s našim kodom, možete promijeniti CircularDependencyA u sljedeće:
@Component javna klasa CircularDependencyA {private CircularDependencyB circB; @Autowired javni CircularDependencyA (@Lazy CircularDependencyB circB) {this.circB = circB; }}
Ako sada pokrenete test, vidjet ćete da se pogreška ovaj put ne događa.
4.3. Koristite Setter / Field Injection
Jedno od najpopularnijih rješenja, a također i ono što proljetna dokumentacija predlaže, je upotreba ubrizgavanja setera.
Jednostavno rečeno, ako promijenite načine povezivanja graha da biste koristili ubrizgavanje setera (ili ubrizgavanje polja) umjesto ubrizgavanja konstruktora - to rješava problem. Na ovaj način Spring stvara grah, ali ovisnosti se ne ubrizgavaju sve dok nisu potrebne.
Učinimo to - promijenimo klase kako bi koristili injekcije postavljača i dodati ćemo drugo polje (poruka) do CircularDependencyB tako da možemo napraviti odgovarajući jedinični test:
@Component javna klasa CircularDependencyA {private CircularDependencyB circB; @Autowired javna praznina setCircB (CircularDependencyB circB) {this.circB = circB; } public CircularDependencyB getCircB () {return circB; }}
@Component javna klasa CircularDependencyB {private CircularDependencyA circA; privatna niska poruka = "Bok!"; @Autowired javna praznina setCircA (CircularDependencyA circA) {this.circA = circA; } javni String getMessage () {return poruka; }}
Sada moramo unijeti neke promjene u naš jedinični 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 javna praznina givenCircularDependency_whenSetterInjection_thenItWorks () {CircularDependencyA circA = context.getBean (CircularDependencyA.class); Assert.assertEquals ("Bok!", CircA.getCircB (). GetMessage ()); }}
Sljedeće objašnjava gore navedene bilješke:
@Grah: Da kažem Spring framework-u da se ove metode moraju koristiti za dohvaćanje implementacije graha za ubrizgavanje.
@Test: Test će dobiti CircularDependencyA grah iz konteksta i ustvrditi da je njegova CircularDependencyB pravilno ubrizgana, provjeravajući vrijednost njegovog poruka imovine.
4.4. Koristiti @PostConstruct
Drugi način prekida ciklusa je ubrizgavanje ovisnosti pomoću @Autowired na jednom od zrna, a zatim upotrijebite metodu označenu s @PostConstruct postaviti drugu ovisnost.
Naš grah mogao bi imati sljedeći kod:
@Component javna klasa CircularDependencyA {@Autowired private CircularDependencyB circB; @PostConstruct public void init () {circB.setCircA (this); } public CircularDependencyB getCircB () {return circB; }}
@Component javna klasa CircularDependencyB {private CircularDependencyA circA; privatna niska poruka = "Bok!"; javna praznina setCircA (CircularDependencyA circA) {this.circA = circA; } javni String getMessage () {return poruka; }}
A možemo pokrenuti isti test koji smo prethodno imali, pa provjeravamo da li se iznimka kružne ovisnosti još uvijek ne baca i jesu li ovisnosti pravilno ubrizgane.
4.5. Implementirati ApplicationContextAware i InitializingBean
Ako jedan od graha provodi ApplicationContextAware, grah ima pristup kontekstu Spring i odatle može izvući drugi grah. Provedba InitializingBean naznačujemo da ovaj grah mora izvršiti neke radnje nakon što su postavljena sva njegova svojstva; u ovom slučaju želimo ručno postaviti svoju ovisnost.
Kod našeg graha bio bi:
@Component javna klasa CircularDependencyA implementira ApplicationContextAware, InitializingBean {private CircularDependencyB circB; privatni kontekst ApplicationContext; public CircularDependencyB getCircB () {return circB; } @Override public void afterPropertiesSet () baca iznimku {circB = context.getBean (CircularDependencyB.class); } @Override public void setApplicationContext (final ApplicationContext ctx) baca BeansException {context = ctx; }}
@Component javna klasa CircularDependencyB {private CircularDependencyA circA; privatna niska poruka = "Bok!"; @Autowired javna praznina setCircA (CircularDependencyA circA) {this.circA = circA; } javni String getMessage () {return poruka; }}
Opet, možemo pokrenuti prethodni test i vidjeti da se iznimka ne izbacuje i da test radi kako se očekivalo.
5. U zaključku
Postoji mnogo načina za rješavanje kružnih ovisnosti u proljeće. Prvo što treba uzeti u obzir jest redizajnirati svoj grah tako da nema potrebe za kružnim ovisnostima: oni su obično simptom dizajna koji se može poboljšati.
Ali ako apsolutno trebate imati kružne ovisnosti u svom projektu, možete slijediti neke od ovdje predloženih zaobilaznih rješenja.
Poželjna metoda je korištenje injekcija setera. Ali postoje i druge alternative, koje se uglavnom temelje na zaustavljanju Springa da upravlja inicijalizacijom i ubrizgavanjem graha i na tome da to sami radite koristeći jednu ili drugu strategiju.
Primjeri se mogu naći u projektu GitHub.