典型場景:用戶 與 群組 為多對多關聯。User 持有 Set<Group> groups,Group 持有 Set<User> users。當你嘗試將查詢出的 PageBean<User> 序列化為 JSON 時:
User → 遍歷 groupsGroup → 遍歷 usersUser → 回到第 1 步 → 無限遞歸最終拋出:net.sf.json.JSONException: There is a cycle in the hierarchy!
使用 json-lib 的 JsonConfig 顯式聲明「序列化 User 時忽略 groups,序列化 Group 時忽略 users」:
PageBean<User> resultUser = userService.findUserByUse(evenFind);
JSONArray jsonArray = new JSONArray();
JSONObject jsonObject = new JSONObject();
if (resultUser != null && !resultUser.getData().isEmpty()) {
for (User user : resultUser.getData()) {
JsonConfig jsonConfig = new JsonConfig();
jsonConfig.setIgnoreDefaultExcludes(true);
jsonConfig.setCycleDetectionStrategy(CycleDetectionStrategy.LENIENT); // 寬容模式:遇到循環不拋異常,直接跳過
// 核心:指定要排除的屬性名(對應實體類的字段名)
jsonConfig.setExcludes(new String[]{"groups"}); // User 端排除 groups
JSONObject json = JSONObject.fromObject(user, jsonConfig);
jsonArray.add(json);
}
jsonObject.put("data", jsonArray);
jsonObject.put("pageSize", resultUser.getPageSize());
jsonObject.put("pageNum", resultUser.getPageNum());
}
優點:精確控制,不影響其他屬性,業務邏輯清晰。
缺點:需手動維護排除列表,實體類字段變更需同步修改。
若使用 Fastjson 或 Jackson,直接在實體類字段上打標:
// Fastjson 風格
public class User {
// ...
@JSONField(serialize = false) // 序列化時忽略
private Set<Group> groups;
}
// Jackson 風格
public class Group {
// ...
@JsonIgnore // 雙向忽略
private Set<User> users;
}
優點:聲明式,與實體定義在一起,重構友好,無運行時代碼。
缺點:強依賴特定 JSON 庫,若專案混用多庫需重複標記。
根治思路:實體類只做 ORM 映射,絕不直接暴露給外部序列化。定義專門的 UserVO、GroupVO 作為傳輸對象,僅包含前端需要的字段,彻底切斷雙向關聯。
// VO 類,無雙向引用,結構扁平
public class UserVO {
private Long id;
private String username;
private List<SimpleGroupVO> groups; // 只包含群組 ID 和名稱
// getter/setter
}
public class SimpleGroupVO {
private Long id;
private String name;
// getter/setter
}
// Service 層轉換
public PageBean<UserVO> findUserVO(QueryParam param) {
PageBean<User> entityPage = userDao.findPage(param);
List<UserVO> voList = entityPage.getData().stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return new PageBean<>(voList, entityPage.getTotalRecord(), ...);
}
private UserVO convertToVO(User user) {
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
vo.setGroups(user.getGroups().stream()
.map(g -> new SimpleGroupVO(g.getId(), g.getName()))
.collect(Collectors.toList()));
return vo;
}
優點:
缺點:增加轉換代碼量(可用 MapStruct 自動生成)。
| 方案 | 適用場景 | 維護成本 | 性能 |
|---|---|---|---|
| JsonConfig 排除 | 快速修復、老項目 json-lib、不想動實體類 | 中(需同步字段名) | 好(運行時動態過濾) |
| 註解標記 | 新項目用 Fastjson/Jackson、團隊統一技術棧 | 低(聲明式) | 優(編譯期決定) |
| DTO/VO 模式 | 中大型項目、對外開放 API、需精確控制輸出 | 高(初期投入) | 最優(無反射、無多餘字段) |
即使斷開了循環,若關聯集合是 LAZY 且 Session 已關閉,訪問時會拋 LazyInitializationException。三種應對:
@NamedEntityGraph 標準化支持。@Transactional,確保轉換時 Session 仍開啟。JSON 死循環本質是領域模型的雙向關聯與樹狀序列化格式的本質衝突。
臨時救火用 JsonConfig.setExcludes() 或 @JsonIgnore;
長久之計請堅定推行 DTO/VO 模式,實現「持久化模型」與「傳輸模型」的徹底分離。這不僅解決了循環引用,更是構建穩健、可演進 API 的基石。