Fastjson1 循环引用检测与 $ ref 解决方案
在 Fastjson 1.x 中,循环引用检测是默认开启的,这虽然保护了内存溢出,但在实际开发(尤其是前后端分离)中经常导致数据格式“异常”。以下是关于该机制的详细解析和解决方案
什么是循环引用?
在 Java 对象图中,如果对象 A 引用了对象 B,而对象 B 又引用了对象 A(或者 A 引用了自己),这就构成了循环引用。
Fastjson 的默认行为:
为了防止序列化时无限递归导致 StackOverflowError(栈溢出),Fastjson 默认开启了循环引用检测。当它发现同一个对象被多次引用时,第一次会正常输出 JSON,后续的引用会被替换为一个特殊的标记:{"$ref": "..."}。
问题场景:前端无法识别 $ref
虽然 {"$ref": "..."} 能够保持对象引用的完整性,但绝大多数前端框架(JavaScript/TypeScript)并不识别这种格式,导致前端拿到的数据不是预期的 JSON 对象,而是包含 $ref 的奇怪结构,从而引发页面渲染错误。
代码演示:复现 $ref 问题
假设我们有两个类,User 和 Department,它们互相引用:
public class User {
private Integer id;
private String name;
private Department department;
// 构造器、Getter、Setter 省略
}
public class Department {
private Integer id;
private String deptName;
private User manager; // 部门经理是 User
// 构造器、Getter、Setter 省略
}
测试代码(默认配置):
User user = new User(1, "张三");
Department dept = new Department(101, "研发部");
// 互相引用
user.setDepartment(dept);
dept.setManager(user);
// 使用 Fastjson 默认序列化
String jsonStr = JSON.toJSONString(user);
System.out.println(jsonStr);
输出结果(包含 $ref):
{
"department": {
"deptName": "研发部",
"id": 101,
"manager": {"$ref": ".."}
},
"id": 1,
"name": "张三"
}
注意: "manager": {"$ref": ".."} 表示 manager 引用了它的上级对象(即 user 本身)。虽然逻辑正确,但前端拿到这个 JSON 会很难处理。
出现 StackOverflowError(栈溢出)的原因非常直接:当你关闭了循环引用检测,Fastjson 就会变成一个“一根筋”的老实人。它看到 user 里有 department,就去序列化 department;发现 department 里又有 user,就接着去序列化 user…… 如此 A -> B -> A -> B 无限套娃,直到把程序的栈内存彻底撑爆。
循环引用解决方案
忽略这个字段
循环引用是 Department 里的 manager 字段引起的,而前端往往并不需要这个反向关联,我们直接在实体类的字段上加个 @JSONField(serialize = false) 注解,告诉 Fastjson:“序列化时直接无视这个字段”。
修改实体类:
import com.alibaba.fastjson.annotation.JSONField;
public Department {
private Integer;
private String deptName;
// 加上这个注解,序列化时直接跳过 manager 字段,彻底打断循环链条
@JSONField(serialize = false)
private User manager;
// ... 省略其他 getter/setter
}
序列化代码
import com.alibaba.fastjson.JSON;
// ...
// 直接正常序列化,因为 manager 字段已经被我们屏蔽了,不会触发循环
String jsonStr = JSON.toJSONString(user);
System.out.println(jsonStr);
使用 DTO 数据传输对象
在实际的 Web API 开发中,直接把带双向引用的数据库实体(Entity)返回给前端本身就是大忌。最规范的做法是创建一个专门给前端用的 DTO 对象,只保留前端需要的字段。
新建一个 UserDTO 类:
public UserDTO {
private Integer id;
private String name;
private String deptName; // 只保留前端需要的部门名称,不保留整个 Department 对象
// 构造器、Getter、Setter 省略
}
序列化代码:
import com.alibaba.fastjson.JSON;
// ...
// 手动把 Entity 转成没有循环引用的 DTO
UserDTO userDTO = new UserDTO(user.getId(), user.getName(), user.getDepartment().getDeptName());
// 序列化 DTO,结构扁平清晰,没有任何循环引用的风险
String jsonStr = JSON.toJSONString(userDTO);
System.out.println(jsonStr);
$ref 机制是 Fastjson 保护你的代码不崩溃的安全机制,而不是 Bug。与其强行禁用引发风险,不如在编码规范层面避免循环引用,或者让前端适配这个标准格式。 
