Tới nội dung chính
caltalys.dev
Thời điểm / Lifecycle

BeanCurrentlyInCreationException - và cái cờ mà Spring khóa lại vì lý do chính đáng

Đổi sang field injection thì hết lỗi, bật allow-circular-references thì chạy thật. Nhưng lúc mở cái cờ mà Spring chủ động khoá lại, bạn đang mở cái gì?

Đồ hình trừu tượng của tập 5.1 trong Quyển Một
Caltalys 7 phút đọc

Thằng em giờ gần như không cần mình nữa. Tin nhắn của nó bây giờ không phải câu hỏi - là báo cáo điều tra kèm đúng một chỗ tắc:

thang em

Anh, app em không boot. Boot in cho em hẳn một sơ đồ vòng tròn tử tế: The dependencies of some of the beans in the application context form a cycle - OrderService cần PaymentService, PaymentService cần lại OrderService. Em hiểu vòng lặp. Cái em không hiểu là ba câu trả lời em tìm được:

Một - mấy câu SO đời 2018 bảo: đổi constructor injection sang field injection là chạy. Em thử. Vẫn chết. Hai - có người bảo bật spring.main.allow-circular-references=true. Em thử. Chạy thật. Ba - nhưng em học series này đủ lâu: một cái cờ mà Spring phải chủ động khóa mặc định, thì lúc mở ra em đang mở cái gì?

Cả ba câu đều dẫn về cùng một nơi, và nơi đó là chỗ khép vòng đẹp nhất của series: chúng ta quay về đúng cái xưởng đúc bean của bài đầu tiên - lần này nhìn vào một ngăn mà hôm đó mình cố tình không mở.

Nhớ lại: getBean() chạy đủ bảy chặng. Giờ cho nó một vòng tròn. Tạo OrderService - chặng (1), constructor - mà constructor đòi PaymentService. Container tạm dừng A, đi tạo B. Constructor của B đòi… A. Container quay lại tạo A - và khựng: A đang được tạo dở, chính tôi đang cầm nó trên tay. Đệ quy tự cắn đuôi. BeanCurrentlyInCreationException - với constructor injection, chết ở đây, sớm và ồn ào, không có gì để bàn: chưa qua constructor thì chưa tồn tại cái gì để đưa cho ai cả.

Vậy sao field injection từng thoát được? Vì cái ngăn giấu kín kia. Xưởng đúc của Spring thật ra có ba tầng kệ: tầng một - thành phẩm, bean đã qua đủ bảy chặng, ai hỏi cũng phát. Tầng hai và ba - gọi gộp là quầy cho-mượn-sớm: ngay sau khi constructor xong - mới chặng (1), chưa inject, chưa @PostConstruct, chưa gì hết - Spring đã đặt lên đó một lời hứa: “ai kẹt vòng tròn thì mượn tạm thằng này, bản chưa lắp xong.”

Với field injection, vòng tròn đi thế này: A qua constructor (constructor rỗng - đó là toàn bộ “phép màu” của field injection: nó hoãn nhu cầu sang chặng (2)) → A lên quầy cho-mượn-sớm → sang tạo B → B cần A ở chặng inject → quầy cho mượn bản A-chưa-hoàn-thiện → B lắp xong, thành phẩm → quay về lắp nốt A. Vòng tròn được phá bằng một khoản nợ: có một khoảnh khắc, B cầm trong tay một A chưa đi hết dây chuyền.

Và bạn - sau bốn mùa - đủ trình để tự hỏi câu mà dev 2018 không hỏi: chưa đi hết dây chuyền, nghĩa là chưa qua chặng nào? Chặng (7). Chặng bọc proxy. Cái A mà B mượn có nguy cơ là bản trần - chưa vỏ. Nếu chuyện dừng ở đó, @Transactional của A sẽ vô hình với riêng B - đúng chủng loại bug im lặng mà cả series này săn lùng. Spring biết, và có hẳn cơ chế vá: quầy cho-mượn-sớm không đưa thẳng bản trần, nó hỏi trước cái List<BeanPostProcessor> - lại là cái List ấy - “ai cần bọc thì bọc ngay bây giờ, sớm hơn lịch”. Vá được phần lớn. Nhưng “phần lớn” trong một khu vực chằng chịt proxy, AOP, và thứ tự khởi tạo là một cụm từ khiến chính Spring team mất ngủ - mỗi ca lệch là một bug ma xuất hiện tùy cấu hình, tùy thứ tự bean, không tái hiện nổi.

Nên đến Boot 2.6, họ làm một việc mà bạn đã thấy triết lý của nó ở bài cái chuông báo cháy: đổi im lặng lấy ồn ào. allow-circular-references khóa mặc định; mọi vòng tròn - kể cả loại field injection từng “chạy” - giờ chết ngay lúc boot, kèm sơ đồ vòng vẽ sẵn. Câu SO 2018 không sai thời của nó; nó chỉ mô tả một thế giới mà Spring đã chủ động đóng cửa. Và câu trả lời cho thằng em: cái cờ kia mở lại khoản nợ cho-mượn-đồ-chưa-lắp-xong - chạy được, đa số ổn, và mọi rủi ro đều thuộc loại không kêu. Bật nó là một quyết định, không phải một cái fix.

Còn cặp bài trùng hôm nay - cái fix đường hoàng hơn: @Lazy.

public OrderService(@Lazy PaymentService paymentService) { ... }

Vòng tròn tắt. Vì sao? Vì @Lazy ở vị trí inject nghĩa là: đừng đưa tôi PaymentService thật - đưa tôi một người đóng thế. Đúng, lại là nhân vật của mùa một, lần cuối cùng lên sân khấu: Spring đúc một proxy rỗng, chỉ ôm mỗi lời hứa “lần đầu có ai gọi tôi, tôi sẽ ra container lấy bản thật”. Lúc wiring, chẳng ai cần B thật nữa - nhu cầu bị dời từ lúc lắp ráp sang lúc sử dụng đầu tiên, mà lúc sử dụng thì cả hai bean đã thành phẩm từ lâu. Vòng tròn không bị phá - nó bị dời sang một trục thời gian khác, nơi nó không còn là vòng tròn. Bạn nhận ra chưa: @Lazy và quầy cho-mượn-sớm giải cùng một bài, nhưng quầy cho mượn vật thật chưa xong, còn @Lazy cho mượn tờ giấy hẹn - và sau bài LazyInitialization, bạn thừa biết giấy hẹn an toàn hơn vật dở dang, miễn là biết giờ đóng cửa.

Nói cho trọn: cả allow-circular-references lẫn @Lazy đều là thuốc giảm đau. Vòng tròn phụ thuộc gần như luôn là triệu chứng của một chuyện thiết kế - hai service ôm nhau thường vì có một trách nhiệm thứ ba chưa được đặt tên. Tách nó ra thành bean C, cho cả A và B cùng trỏ vào, vòng thành hình quạt, và bạn không cần cờ nào nữa. Boot vẽ sẵn sơ đồ vòng cho bạn ngay trong thông báo lỗi - nó không chỉ báo bệnh, nó đưa luôn phim chụp.

Muốn tự thấy tận mắt: tạo vòng A↔B constructor injection, boot, đọc cái sơ đồ ASCII - rồi thêm @Lazy vào một phía và đặt breakpoint xem class của tham số được inject: một cái tên proxy quen thuộc sẽ chào bạn như người quen cũ. Còn muốn nhìn ba tầng kệ bằng mắt: breakpoint trong DefaultSingletonBeanRegistry.getSingleton() - ba cái Map nằm đó, tên đúng như truyện: singletonObjects, earlySingletonObjects, singletonFactories.

Nguyên lý, thứ sống lâu hơn Spring: mọi hệ khởi tạo có phụ thuộc vòng chỉ có ba lối ra - cấm vòng, cho mượn bản dở dang, hoặc thay một cạnh bằng lời hứa. Linker xử lý symbol vòng, module system xử lý import vòng, build tool xử lý task vòng - đều là ba lối đó, đổi tên. Và lối “cho mượn bản dở dang” ở đâu cũng là lối lắm ma nhất.

Thằng em chốt:

Cái cờ không mở tính năng - nó mở lại một khoản vay mà Spring đã quyết định thôi không bảo lãnh.

thang em Vậy em hiểu rồi. Cái cờ không mở một tính năng - nó mở lại một khoản vay mà Spring đã quyết định thôi không đứng ra bảo lãnh nữa.

Đúng rồi đó. Còn ba bài nữa là em ra trường. 😃

Tài nguyên tập 18

Cheat-sheet tập 18 Toàn bộ cơ chế trên một trang - in ra hoặc lưu về máy được. Source demo - tập 5.1JDK 17. mvn spring-boot:run → Episode51Runner in khối [EP 5.1]: bốn nhịp cho cùng một vòng tròn hai bean - constructor injection, field injection, allowCircularReferences, và @Lazy một phía.

Vị trí trong Lớp Trung Gian Vô Hình

Bài này trả lời

  • Lỗi phụ thuộc vòng giữa các bean
Bản in
Chia sẻ: Facebook X LinkedIn Email
Nhận bài mới qua email
Powered by follow.it