Sự phụ thuộc vào vòng tròn trong mùa xuân

1. Sự phụ thuộc vào vòng tròn là gì?

Điều đó xảy ra khi một hạt đậu A phụ thuộc vào một hạt đậu khác B, và hạt đậu B cũng phụ thuộc vào hạt đậu A:

Đậu A → Đậu B → Đậu A

Tất nhiên, chúng ta có thể có nhiều đậu hơn ngụ ý:

Đậu A → Đậu B → Đậu C → Đậu D → Đậu E → Đậu A

2. Điều gì sẽ xảy ra vào mùa xuân

Khi bối cảnh Spring đang tải tất cả các bean, nó sẽ cố gắng tạo các bean theo thứ tự cần thiết để chúng hoạt động hoàn toàn. Ví dụ: nếu chúng ta không có phụ thuộc vòng tròn, như trường hợp sau:

Đậu A → Đậu B → Đậu C

Spring sẽ tạo bean C, sau đó tạo bean B (và tiêm bean C vào đó), sau đó tạo bean A (và tiêm bean B vào đó).

Tuy nhiên, khi có một phụ thuộc vòng tròn, Spring không thể quyết định hạt đậu nào nên được tạo trước, vì chúng phụ thuộc vào nhau. Trong những trường hợp này, Spring sẽ tạo ra một BeanCurrentlyInCreationException trong khi tải ngữ cảnh.

Nó có thể xảy ra trong Spring khi sử dụng hàm tạo chèn ; nếu bạn sử dụng các loại tiêm khác, bạn sẽ không thấy vấn đề này vì các phần phụ thuộc sẽ được tiêm khi cần thiết chứ không phải khi tải ngữ cảnh.

3. Một ví dụ nhanh

Hãy xác định hai bean phụ thuộc vào nhau (thông qua phương thức tiêm hàm tạo):

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

Bây giờ chúng ta có thể viết một lớp Cấu hình cho các bài kiểm tra, hãy gọi nó là TestConfig , chỉ định gói cơ sở để quét các thành phần. Giả sử các bean của chúng ta được định nghĩa trong gói “ com.baeldung.circulardependency ”:

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

Và cuối cùng chúng ta có thể viết một bài kiểm tra JUnit để kiểm tra sự phụ thuộc của vòng tròn. Kiểm tra có thể để trống, vì sự phụ thuộc vòng tròn sẽ được phát hiện trong quá trình tải ngữ cảnh.

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

Nếu bạn cố gắng chạy thử nghiệm này, bạn sẽ nhận được ngoại lệ sau:

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

4. Giải pháp thay thế

Chúng tôi sẽ chỉ ra một số cách phổ biến nhất để giải quyết vấn đề này.

4.1. Thiết kế lại

Khi bạn có sự phụ thuộc vòng tròn, có khả năng bạn gặp vấn đề về thiết kế và các trách nhiệm không được phân tách rõ ràng. Bạn nên cố gắng thiết kế lại các thành phần một cách hợp lý để hệ thống phân cấp của chúng được thiết kế tốt và không cần phụ thuộc vòng tròn.

Nếu bạn không thể thiết kế lại các thành phần (có thể có nhiều lý do: mã kế thừa, mã đã được kiểm tra và không thể sửa đổi, không đủ thời gian hoặc nguồn lực để thiết kế lại hoàn chỉnh…), có một số cách giải quyết để thử.

4.2. Sử dụng @Lazy

Một cách đơn giản để phá vỡ chu kỳ là nói Spring để khởi tạo một trong các bean một cách lười biếng. Đó là: thay vì khởi tạo đầy đủ bean, nó sẽ tạo một proxy để đưa nó vào bean khác. Hạt đậu được tiêm sẽ chỉ được tạo ra đầy đủ khi nó cần lần đầu tiên.

Để thử điều này với mã của chúng tôi, bạn có thể thay đổi CircularDependencyA thành như sau:

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

Nếu bạn chạy thử nghiệm ngay bây giờ, bạn sẽ thấy rằng lỗi không xảy ra lần này.

4.3. Sử dụng Setter / Field Injection

Một trong những cách giải quyết phổ biến nhất và cũng là những gì tài liệu Spring đề xuất, là sử dụng setter injection.

Nói một cách đơn giản nếu bạn thay đổi cách các hạt đậu của bạn được kết nối để sử dụng bộ tiêm setter (hoặc tiêm trường) thay vì tiêm hàm tạo - điều đó sẽ giải quyết được vấn đề. Bằng cách này, Spring tạo ra các bean, nhưng các phụ thuộc không được tiêm cho đến khi chúng cần thiết.

Hãy làm điều đó - hãy thay đổi các lớp của chúng tôi để sử dụng bộ định tuyến và sẽ thêm một trường ( thông báo ) khác vào CircularDependencyB để chúng tôi có thể thực hiện kiểm tra đơn vị thích hợp:

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

Bây giờ chúng ta phải thực hiện một số thay đổi đối với bài kiểm tra đơn vị của mình:

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

Phần sau giải thích các chú thích ở trên:

@Bean : Để nói với Spring framework rằng các phương thức này phải được sử dụng để truy xuất việc triển khai các bean để đưa vào.

@Test : Bài kiểm tra sẽ nhận được đậu CircularDependencyA từ ngữ cảnh và khẳng định rằng CircularDependencyB của nó đã được đưa vào đúng cách, kiểm tra giá trị của thuộc tính thông báo của nó .

4.4. Sử dụng @PostConstruct

Một cách khác để phá vỡ chu kỳ là chèn một phụ thuộc bằng @Autowntic vào một trong các bean và sau đó sử dụng một phương thức được chú thích bằng @PostConstruct để đặt phụ thuộc kia.

Đậu của chúng tôi có thể có mã sau:

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

Và chúng tôi có thể chạy cùng một thử nghiệm mà chúng tôi đã có trước đó, vì vậy chúng tôi kiểm tra xem ngoại lệ phụ thuộc vòng tròn vẫn chưa được ném ra và các phụ thuộc đã được đưa vào đúng cách chưa.

4.5. Triển khai ApplicationContextAwareInitializingBean

Nếu một trong các bean thực hiện ApplicationContextAware , bean có quyền truy cập vào ngữ cảnh Spring và có thể trích xuất bean khác từ đó. Thực hiện InitializingBean, chúng tôi chỉ ra rằng bean này phải thực hiện một số hành động sau khi tất cả các thuộc tính của nó đã được thiết lập; trong trường hợp này, chúng tôi muốn đặt thủ công phụ thuộc của mình.

Mã đậu của chúng ta sẽ là:

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

Một lần nữa, chúng ta có thể chạy thử nghiệm trước đó và thấy rằng ngoại lệ không được ném ra và thử nghiệm đang hoạt động như mong đợi.

5. Kết luận

Có nhiều cách để xử lý các phụ thuộc vòng tròn trong Spring. Điều đầu tiên cần xem xét là thiết kế lại các hạt đậu của bạn để không cần phụ thuộc vào vòng tròn: chúng thường là dấu hiệu của một thiết kế có thể được cải thiện.

Nhưng nếu bạn thực sự cần có các phụ thuộc vòng tròn trong dự án của mình, bạn có thể làm theo một số cách giải quyết được đề xuất tại đây.

Phương pháp ưa thích là sử dụng tiêm setter. Nhưng có những lựa chọn thay thế khác, thường dựa trên việc ngăn Spring quản lý việc khởi tạo và tiêm các bean và tự mình thực hiện điều đó bằng cách sử dụng chiến lược này hay chiến lược khác.

Các ví dụ có thể được tìm thấy trong dự án GitHub.