首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Hibernate 多對多關聯導致 JSON 序列化死循環?三招徹底斷環

     編輯:Java 綜合教學

現場還原:為什麼會死循環?

典型場景:用戶 與 群組 為多對多關聯。User 持有 Set<Group> groups,Group 持有 Set<User> users。當你嘗試將查詢出的 PageBean<User> 序列化為 JSON 時:

  1. 序列化 User → 遍歷 groups
  2. 序列化 Group → 遍歷 users
  3. 再次遇到同一個 User → 回到第 1 步 → 無限遞歸

最終拋出:net.sf.json.JSONException: There is a cycle in the hierarchy!

方案一:JsonConfig 精準排除反向引用屬性(推薦,最精準)

使用 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());
}

優點:精確控制,不影響其他屬性,業務邏輯清晰。
缺點:需手動維護排除列表,實體類字段變更需同步修改。

方案二:@JSONField / @JsonIgnore 註解靜態標記(最乾淨,零配置代碼)

若使用 Fastjson 或 Jackson,直接在實體類字段上打標:

// Fastjson 風格
public class User {
    // ...
    @JSONField(serialize = false) // 序列化時忽略
    private Set<Group> groups;
}

// Jackson 風格
public class Group {
    // ...
    @JsonIgnore // 雙向忽略
    private Set<User> users;
}

優點:聲明式,與實體定義在一起,重構友好,無運行時代碼。
缺點:強依賴特定 JSON 庫,若專案混用多庫需重複標記。

方案三:DTO/VO 模式徹底解耦(架構級方案,最穩健)

根治思路:實體類只做 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;
}

優點:

  • 徹底消除循環引用可能
  • API 契約穩定,不因數據庫模型變更而波動
  • 可精確控制字段(如隱藏敏感字段 password、內部狀態)
  • 支持字段扁平化、聚合、格式化等展示邏輯

缺點:增加轉換代碼量(可用 MapStruct 自動生成)。

性能與選型建議

方案適用場景維護成本性能
JsonConfig 排除 快速修復、老項目 json-lib、不想動實體類 中(需同步字段名) 好(運行時動態過濾)
註解標記 新項目用 Fastjson/Jackson、團隊統一技術棧 低(聲明式) 優(編譯期決定)
DTO/VO 模式 中大型項目、對外開放 API、需精確控制輸出 高(初期投入) 最優(無反射、無多餘字段)

避坑指南:關於 Hibernate 延遲加載的隱形炸彈

即使斷開了循環,若關聯集合是 LAZY 且 Session 已關閉,訪問時會拋 LazyInitializationException。三種應對:

  1. Open Session In View(OSIV):Web 環境下常用,但易導致 N+1 問題,需謹慎。
  2. FETCH JOIN / EntityGraph:查詢時主動加載關聯,@NamedEntityGraph 標準化支持。
  3. DTO 轉換在事務內完成:Service 方法加 @Transactional,確保轉換時 Session 仍開啟。

總結

JSON 死循環本質是領域模型的雙向關聯與樹狀序列化格式的本質衝突。
臨時救火用 JsonConfig.setExcludes() 或 @JsonIgnore;
長久之計請堅定推行 DTO/VO 模式,實現「持久化模型」與「傳輸模型」的徹底分離。這不僅解決了循環引用,更是構建穩健、可演進 API 的基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved