Ketergantungan Melingkar di Musim Semi

1. Apa Itu Ketergantungan Melingkar?

Itu terjadi ketika kacang A bergantung pada kacang B lainnya, dan kacang B bergantung pada kacang A juga:

Kacang A → Kacang B → Kacang A

Tentu saja, kami dapat memiliki lebih banyak kacang yang tersirat:

Kacang A → Kacang B → Kacang C → Kacang D → Kacang E → Kacang A

2. Apa yang Terjadi di Musim Semi

Saat konteks Musim Semi memuat semua biji, ia mencoba membuat kacang dalam urutan yang diperlukan agar dapat berfungsi sepenuhnya. Misalnya, jika kita tidak memiliki dependensi melingkar, seperti kasus berikut:

Kacang A → Kacang B → Kacang C

Spring akan membuat kacang C, lalu membuat kacang B (dan menyuntikkan kacang C ke dalamnya), lalu membuat kacang A (dan menyuntikkan kacang B ke dalamnya).

Tetapi, ketika memiliki ketergantungan melingkar, Spring tidak dapat memutuskan biji mana yang harus dibuat terlebih dahulu, karena mereka bergantung satu sama lain. Dalam kasus ini, Spring akan memunculkan BeanCurrentlyInCreationException saat memuat konteks.

Itu bisa terjadi di Spring saat menggunakan injeksi konstruktor ; jika Anda menggunakan jenis suntikan lain, Anda tidak akan menemukan masalah ini karena ketergantungan akan disuntikkan saat diperlukan dan bukan pada pemuatan konteks.

3. Contoh Cepat

Mari kita tentukan dua kacang yang bergantung satu sama lain (melalui injeksi konstruktor):

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

Sekarang kita dapat menulis kelas Konfigurasi untuk pengujian, sebut saja TestConfig , yang menetapkan paket dasar untuk memindai komponen. Mari kita asumsikan kacang kita didefinisikan dalam paket " com.baeldung.circulardependency ":

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

Dan akhirnya kita bisa menulis pengujian JUnit untuk memeriksa dependensi melingkar. Tes bisa kosong, karena ketergantungan melingkar akan terdeteksi selama pemuatan konteks.

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

Jika Anda mencoba menjalankan tes ini, Anda akan mendapatkan pengecualian berikut:

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

4. Solusi

Kami akan menunjukkan beberapa cara paling populer untuk mengatasi masalah ini.

4.1. Desain ulang

Ketika Anda memiliki ketergantungan melingkar, kemungkinan Anda memiliki masalah desain dan tanggung jawab tidak dipisahkan dengan baik. Anda harus mencoba mendesain ulang komponen dengan benar sehingga hierarki mereka dirancang dengan baik dan tidak perlu dependensi melingkar.

Jika Anda tidak dapat mendesain ulang komponen (ada banyak kemungkinan alasan untuk itu: kode lama, kode yang telah diuji dan tidak dapat dimodifikasi, tidak cukup waktu atau sumber daya untuk mendesain ulang lengkap…), ada beberapa solusi untuk dicoba.

4.2. Gunakan @Lazy

Cara sederhana untuk memutus siklus adalah mengatakan Spring untuk menginisialisasi salah satu kacang dengan malas. Yaitu: alih-alih menginisialisasi kacang sepenuhnya, ia akan membuat proxy untuk menyuntikkannya ke kacang lain. Kacang yang disuntikkan hanya akan dibuat sepenuhnya saat dibutuhkan pertama kali.

Untuk mencoba ini dengan kode kami, Anda dapat mengubah CircularDependencyA menjadi berikut:

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

Jika Anda menjalankan pengujian sekarang, Anda akan melihat bahwa kesalahan tidak terjadi kali ini.

4.3. Gunakan Setter / Field Injection

Salah satu solusi yang paling populer, dan juga apa yang diusulkan oleh dokumentasi Spring, adalah menggunakan injeksi penyetel.

Sederhananya jika Anda mengubah cara kacang Anda ditransfer untuk menggunakan injeksi penyetel (atau injeksi lapangan) daripada injeksi konstruktor - itu mengatasi masalah. Dengan cara ini Spring membuat kacang, tetapi dependensi tidak disuntikkan sampai dibutuhkan.

Mari kita lakukan itu - mari kita ubah kelas kita untuk menggunakan suntikan penyetel dan akan menambahkan bidang lain ( pesan ) ke CircularDependencyB sehingga kita dapat membuat pengujian unit yang tepat:

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

Sekarang kita harus membuat beberapa perubahan pada pengujian unit kita:

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

Berikut penjelasan dari penjelasan yang terlihat di atas:

@ Bean : Untuk memberi tahu kerangka Spring bahwa metode ini harus digunakan untuk mengambil implementasi kacang yang akan diinjeksi.

@Test : Tes ini akan mendapatkan kacang CircularDependencyA dari konteksnya dan menegaskan bahwa CircularDependencyB-nya telah dimasukkan dengan benar, memeriksa nilai properti pesannya .

4.4. Gunakan @PostConstruct

Cara lain untuk memutus siklus adalah menyuntikkan ketergantungan menggunakan @Autowired pada salah satu kacang, lalu menggunakan metode yang dianotasi dengan @PostConstruct untuk menyetel ketergantungan lainnya.

Kacang kami dapat memiliki kode berikut:

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

And we can run the same test we previously had, so we check that the circular dependency exception is still not being thrown and that the dependencies are properly injected.

4.5. Implement ApplicationContextAware and InitializingBean

If one of the beans implements ApplicationContextAware, the bean has access to Spring context and can extract the other bean from there. Implementing InitializingBean we indicate that this bean has to do some actions after all its properties have been set; in this case, we want to manually set our dependency.

The code of our beans would be:

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

Again, we can run the previous test and see that the exception is not thrown and that the test is working as expected.

5. In Conclusion

Ada banyak cara untuk menangani dependensi melingkar di Spring. Hal pertama yang harus dipertimbangkan adalah mendesain ulang kacang Anda sehingga tidak perlu ketergantungan melingkar: biasanya ini merupakan gejala desain yang dapat diperbaiki.

Tetapi jika Anda benar-benar perlu memiliki dependensi melingkar dalam proyek Anda, Anda dapat mengikuti beberapa solusi yang disarankan di sini.

Metode yang disukai adalah menggunakan injeksi penyetel. Tetapi ada alternatif lain, umumnya berdasarkan menghentikan Spring dari mengelola inisialisasi dan injeksi kacang, dan melakukannya sendiri menggunakan satu strategi atau yang lain.

Contohnya dapat ditemukan di proyek GitHub.