在使用 Gson 將對象轉 JSON 時,java.lang.IllegalStateException: circular reference error 是高頻報錯,典型場景為雙向關聯實體 (如 User ↔ Order、Parent ↔ Child)。本文剖析異常堆疊,提供三種解決方案,並給出最佳實踐建議。
Caused by: java.lang.IllegalStateException: circular reference error
Offending field: methodAccessor
Offending object: preserveType: false, type: interface sun.reflect.MethodAccessor, obj: sun.reflect.DelegatingMethodAccessorImpl@5886f7bf
at com.google.gson.CircularReferenceException.createDetailedException(CircularReferenceException.java:43)
at com.google.gson.JsonSerializationVisitor.visitObjectField(JsonSerializationVisitor.java:115)
...
at com.htsoft.oa.action.structuringdatamanage.TableargAction.get(TableargAction.java:142)
Offending field: methodAccessor:觸發循環的字段名 (可能是反射生成的內部字段)。TableargAction.get:142:業務代碼調用 gson.toJson(obj) 的行號。
// 實體雙向引用
public class Dept {
private List<Emp> emps; // 部門引用員工列表
// getter/setter
}
public class Emp {
private Dept dept; // 員工引用部門 → 形成循環 Dept→Emp→Dept
// getter/setter
}
| 方案 | 實現方式 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 1. transient 關鍵字 | private transient Dept dept; |
簡單、無依賴、序列化/反序列化均忽略 | 字段永久不可序列化,反序列化後為 null,需手動回填 | 字段確實不需要 JSON 輸出 (如反向引用、臨時字段) |
| 2. @Expose 註解 (需配置) | @Expose(serialize = false) + new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() |
精確控制序列化/反序列化行為,可單獨控制 | 需所有類加註解,GsonBuilder 配置固定,侵入性強 | 需精細控制字段輸出的成熟項目 |
| 3. 自定義 TypeAdapter | 實現 TypeAdapter<Dept> 手寫 JSON 邏輯 |
完全控制,可輸出 ID 而非完整對象,性能最優 | 代碼量大,維護成本高 | 高性能要求、複雜嵌套結構、需輸出精簡引用 (如只輸出 deptId) |
public class Emp {
private Long id;
private String name;
// 關鍵:transient 修飾反向引用
private transient Dept dept;
// 為了反序列化後能用,提供輔助方法 (不參與序列化)
public void setDeptRef(Dept dept) { this.dept = dept; }
public Dept getDeptRef() { return dept; }
}
效果:gson.toJson(dept) 正常輸出,emps 中的每個 Emp 不再包含 dept 字段,循環斷開。
public class DeptAdapter extends TypeAdapter<Dept> {
@Override
public void write(JsonWriter out, Dept dept) throws IOException {
if (dept == null) { out.nullValue(); return; }
out.beginObject();
out.name("id").value(dept.getId());
out.name("name").value(dept.getName());
// 關鍵:只輸出員工 ID 列表,不遞歸輸出完整 Emp 對象
out.name("empIds");
out.beginArray();
for (Emp emp : dept.getEmps()) out.value(emp.getId());
out.endArray();
out.endObject();
}
@Override
public Dept read(JsonReader in) throws IOException {
// 反序列化邏輯...
return null;
}
}
// 註冊
Gson gson = new GsonBuilder()
.registerTypeAdapter(Dept.class, new DeptAdapter())
.create();
// Jackson 僅需一個註解
@JsonManagedReference // 正向引用 (序列化)
public class Dept {
@JsonManagedReference
private List<Emp> emps;
}
@JsonBackReference // 反向引用 (不序列化,反序列化時自動回填)
public class Emp {
@JsonBackReference
private Dept dept;
}
// 或全局配置
objectMapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Jackson 對循環引用的內置支持遠優於 Gson,建議新項目優先選 Jackson。
transient 或規劃 TypeAdapter。GsonBuilder().setExclusionStrategies(...) 統一排除某類型字段 (如所有 MethodAccessor 類型)。Offending field,精準修改對應字段。
Gson gson = new GsonBuilder()
.setExclusionStrategies(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
// 跳過所有 sun.reflect 內部類型字段
return f.getDeclaringClass().getName().startsWith("sun.reflect.")
|| f.getName().contains("methodAccessor");
}
@Override
public boolean shouldSkipClass(Class> clazz) {
return clazz.getName().startsWith("sun.reflect.");
}
})
.create();
循環引用本質是對象圖非樹狀結構,JSON 作為樹狀數據格式無法直接表示。通過 transient 斷開引用鏈、@Expose 精細控制、TypeAdapter 輸出精簡引用三種手段,配合異常日誌定位 Offending field,能快速修復線上序列化崩潰。長期看,建議實體層設計避免雙向強引用,或遷移至 Jackson 獲得原生循環引用支持。