QUYỂN MỘT - BỐN KHUÔN MẶT
return user; - Một Dòng Code, Bốn Tầng Trung Gian
Đường đi ra
User -> orders -> user -> orders; Jackson đi theo graph object hai chiều không có ranh giới API.
Tín hiệu cuối
StackOverflowError tính bằng STACK, N+1 tính bằng QUERY, LazyInitializationException tính bằng EXCEPTION. Cùng graph, ba hóa đơn độc lập; chữa cái này không chữa cái kia.
ServletInvocableHandlerMethod.invokeAndHandle()
.invokeAndHandle()Giá trị return chưa phải response body cho tới khi handler nhận nó
RequestMappingHandlerAdapter.invokeHandlerMethod()Chuẩn bị invocable method và danh sách return value handlers.- Controller trả
UserJava method kết thúc, nhưng response serialization chưa bắt đầu. returnValueHandlers.handleReturnValue()Composite duyệt handler theo thứ tự và chọn handler đầu tiên nhận được return type.RequestResponseBodyMethodProcessor@ResponseBody/@RestControllerđưa value vào message converter pipeline.- Model/view path là nhánh khácKhông phải mọi object return đều tự động được Jackson serialize.
writeWithMessageConverters()
Content negotiation chọn converter có thể ghi media type
handleReturnValue()Đánh dấu request handled rồi tạo input/output message.writeWithMessageConverters(returnValue, ...)Xác định body type, producible media types và converter phù hợp.- Jackson HTTP converterBoot 3 dùng mapper Jackson 2 được auto-config hoặc user cung cấp.
- Serializer đọc property/getterGraph entity trở thành traversal graph; association hai chiều tạo cạnh quay lại.
- Recursion không có điểm dừngNếu không có contract chặn, User -> Order -> User lặp đến stack overflow.
Lazy State Tại Serialization Time
Cùng getter có ba kết quả tùy Session/fetch state
- Association đã initializedJackson traverse dữ liệu RAM; graph hai chiều vẫn có thể recursion.
- Lazy + Session đóngProxy/wrapper cần load và ném
LazyInitializationException. - Lazy + OSIV Session mởGetter gọi Session, load SQL ngoài service transaction.
- Persistence context identity mapGiữ object identity nhất quán nhưng không biến graph hai chiều thành tree JSON.
- Vòng lặp trên collectionMỗi lần chạm lazy lần đầu theo owner có thể nở thành N+1 ngay trong serializer.
Hai hóa đơn, đừng gộp: cái vòng tính tiền bằng stack - từ nhịp thứ hai trở đi nó không tốn thêm câu SQL nào, vì cả hai đầu đã nằm trong identity map. Cái bề rộng của đồ thị lazy mới tính tiền bằng query. Và Jackson duyệt theo chiều sâu: vòng khép lại ngay tại phần tử con đầu tiên, nên chỉ 2 câu SQL kịp chạy trước cú sập (đo trên 1 khách hàng / 3 đơn / 2 dòng mỗi đơn; sau khi cắt vòng, serialize cả cây tốn 8). Ai bật query count mà chờ một trận storm sẽ đếm được 2 và tưởng thí nghiệm hỏng.
Phép kiểm: lấy stacktrace từ frame đầu của RequestResponseBodyMethodProcessor tới Jackson và lazy initializer. Cái bạn catch được là JsonMappingException; phải bóc getCause() mới thấy StackOverflowError và hơn một nghìn frame Jackson. Bật query count, tắt/bật OSIV có chủ đích, rồi test graph có quan hệ hai chiều với mapper thực tế của HTTP converter.
Đặt boundary trước serializer
- Map entity -> DTO trong transactionFetch đúng data và vẽ JSON tree có chủ đích.
- Viết contract test cho response shapeAssert field, query count và không recursion.
- Giữ annotation Jackson ở API modelKhông biến entity mapping thành global wire contract nếu không chủ ý.
Khi KHÔNG được kết luận
@JsonIgnorelà fix tổng quátNó sửa global serialization contract của entity và có thể làm API khác mất field.EAGERchữa recursionNó chỉ đổi thời điểm load; graph hai chiều vẫn là vòng.- Tắt OSIV chữa mọi triệu chứngNó đổi SQL ẩn thành lazy exception; boundary entity vẫn sai.
IDENTITY so với SEQUENCE bằng đồng hồ trên H2 in-memory thì khoảng cách gần như biến mất (35ms so với 15ms) - phải đọc số lần prepare statement mới thấy đúng khoảng cách (200 so với 6). Và đi tìm "nguồn nào đang thắng" trong /actuator/env thì luôn nhận về configurationProperties, một cái tên trông như nguồn nhưng thật ra là cái vỏ Boot đặt lên để nhìn xuyên cả danh sách. Không lần nào cơ chế trong bài sai. Đây cũng chính là câu chuyện của cả quyển, chỉ khác một điều: lần này lớp trung gian nằm ở cái thước đo, không phải ở cái được đo.