1. 簡介
本快速教程將探討如何避免在非已獲取的懶加載對象上使用 Jackson 序列化,使用 Baeldung University 域模型。我們將設置一個簡單的基於 Spring 的應用程序來演示這些原則,但無需任何 Spring 經驗即可跟上。所有示例都僅依賴於 JPA 概念。
2. 設置示例
當使用 JPA 和 Jackson 處理包含延遲關聯的實體時,一個常見的問題是序列化實體。 我們將從一個大學部門的表示開始,該部門包含許多課程:
@Entity
class Department {
@Id
Long id;
String name;
@OneToMany(mappedBy = "department")
List<Course> courses;
// getters and setters
}與 課程 的關聯通過 部門 字段在 課程 中映射。 此外,由於許多一對多關係的默認 fetch 類型是 EAGER, 我們還需要顯式地將 fetch 類型設置為 LAZY。
@Entity
class Course {
@Id
Long id;
String name;
@ManyToOne(fetch = FetchType.LAZY)
Department department;
// getters and setters
}當 Jackson 序列化一個實體時,它會調用其所有 getter 方法。 如果字段仍然是惰性加載的,這會觸發額外的數據庫查詢。 並且如果持久化上下文已關閉,序列化將失敗並拋出 LazyInitializationException。
3. 演示問題
為了可視化該問題,我們將創建端點以保存和檢索 部門 和 課程 實體。
3.1. 測試 DepartmentController
讓我們從具有簡單 POST 和 GET 端點的控制器開始:
@RestController
@RequestMapping("/departments")
public class DepartmentController {
@Autowired
DepartmentRepository repository;
@PostMapping
Department post(@RequestBody Department department) {
return repository.save(department);
}
@GetMapping("/{id}")
Optional<Department> get(@PathVariable("id") Long id) {
return repository.findById(id);
}
}即使我們不包含任何課程,嘗試檢索一個新創建的部門也會導致異常:
curl http://localhost:8080/departments/1那是因為 Jackson 無法初始化被代理的字段:
failed to lazily initialize a collection of role:
com.baeldung.jacksonlazyfields.model.Department.courses:
could not initialize proxy - no Session3.2. 測試 CourseController
我們將遵循相同的模式來測試一個課程控制器,它將包含簡單的 POST 和 GET 端點。 但是 這一次,我們只會針對包含部門的課程拋出異常。 我們可以通過插入一個新的部門來驗證這一點:
curl -X POST http://localhost:8080/departments\
-H "Content-Type: application/json" \
-d '{"name":"Computer-Science"}'然後,通過ID引用它在一個新的課程中:
curl -X POST http://localhost:8080/courses \
-H "Content-Type: application/json" \
-d '{"name":"Machine-Learning", "department":{"id":1} }'如果我們嘗試檢索該課程,將會得到與第一個類似的異常:
could not initialize proxy [com.baeldung.jacksonlazyfields.model.Department#1]
- no Session讓我們看看我們有哪些選擇來避免這種情況。
4. 使用 @JsonIgnore
如果不需要序列化特定的關聯關係,則可以在字段上添加 @JsonIgnore 註解:
@JsonIgnore
@OneToMany(mappedBy = "department")
List<Course> courses;現在,該字段已從序列化中排除:
{
"id": 1,
"name": "Computer-Science"
}這對於在報文中隱藏字段非常有用。
5. 使用 Jackson 的 Hibernate 模塊
全局解決方案是為 Jackson 註冊 Hibernate 模塊,該模塊依賴於 jackson-datatype-hibernate6:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate6</artifactId>
</dependency>在將依賴項包含到我們的 POM 中後,我們必須確保我們正在使用的 ObjectMapper 實例註冊了 Hibernate6Module:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Hibernate6Module());這避免了懶加載;相反,Jackson現在將未獲取的關聯寫為null:
{
"id": 1,
"name": "Computer-Science",
"courses": null
}這個解決方案很棒,因為它適用於使用該 ObjectMapper 實例序列化的任何對象。
6. 使用 DTO 投影
一種繁瑣但可靠的方法是創建 DTO。 通過這種方式,我們可以完全控制序列化的內容,同時解耦持久化層,避免了引入外部庫的需求。
6.1. 創建 DTO
首先,讓我們創建一個只包含我們想要序列化的字段的記錄:
public record DepartmentDto(Long id, String name) {}6.2. 更新 GET 端點
其次,我們將更新我們的 GET 端點,使其使用 DTO:
@GetMapping("/{id}")
Optional get(@PathVariable("id") Long id) {
return repository.findById(id)
.map(d -> new DepartmentDto(id, d.getName()));
}6.3. 獲取結果
現在,Jackson 即使遇到未獲取的字段也不會出現問題:
{
"id": 1,
"name": "Computer-Science"
}7. 結論
在本文中,我們學習瞭如何防止 Jackson 在序列化懶加載關係時訪問未獲取的代理。最佳方法取決於我們的設計:DTOs 最乾淨,而註冊 Hibernate 模塊可以防止意外異常。僅僅添加 @JsonIgnore 確保字段永遠不會被序列化。