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
}
與 課程 的關係通過 department 字段在 Course 中映射的。 此外,我們必須明確設置 fetch 類型為 LAZY,因為對於 one-to-many 關係,默認是 EAGER:
@Entity
class Course {
@Id
Long id;
String name;
@ManyToOne(fetch = FetchType.LAZY)
Department department;
// getters and setters
}
當 Jackson 序列化一個實體時,它會調用所有它的 getter 方法。 如果字段仍然是延遲加載的,這會觸發額外的數據庫查詢。 並且如果持久性上下文已關閉,則序列化失敗,並拋出 LazyInitializationException。
3. 演示問題
為了可視化問題,我們將創建端點來保存和檢索
3.1. 測試
讓我們從具有簡單 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 Session
3.2. 測試
我們將遵循相同的模式來創建一個課程控制器,具有簡單的 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
@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 確保字段永遠不會被序列化。