如何通过Jackson JSON和Hibernate实现递归属性的完全反序列化?
我使用SpringBoot 2.3.9搭配Hibernate,两张表为N-N关系,已完成单向映射。ObjectB包含自引用列,通过Jackson进行递归序列化时,仅首次出现的setObjectB属性被完整序列化,后续仅显示ID,需要所有条目都返回完整POJO。
实体映射代码
ObjectA 实体
@Entity @Table(name = "TABLE_OBJECT_A") public class ObjectA { // 映射N-N关系 @JoinTable(name = "OBJECTA_X_OBJECTB", joinColumns = @JoinColumn(name = "A_ID"), inverseJoinColumns = @JoinColumn(name = "B_ID") ) private Set<ObjectB> setObjectB = new HashSet<ObjectB>(); }
ObjectB 实体
@Entity @Table(name = "TABLE_OBJECT_B") @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") // 设置为false是因为需要完整对象 @JsonIdentityReference(alwaysAsId = false) public class ObjectB { // 自引用字段 @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "B_RECURSION_ID") private ObjectB objectBRecursion; }
JPA findAll的JSON响应
[ { "version": "2023-05-09T16:39:47.082+00:00", "id": 1619, "code": "TRSU", "setObjectB": [ { "id": 31, "version": "2018-07-18T08:12:14.000+00:00", "code": "AMO" }, { "id": 100, "version": "2023-04-18T10:00:45.786+00:00", "code": "SMO" } ] }, { "version": "2023-05-09T16:33:25.404+00:00", "id": 1520, "codice": "SD", "setObjectB": [ 100 ] }, { "version": "2023-05-09T16:40:48.313+00:00", "id": 1620, "code": "TRSE", "setObjectB": [ 31, 100 ] }, { "version": "2022-09-26T08:00:04.127+00:00", "id": 1019, "code": "test", "setObjectB": [ 31 ] }, { "version": "2023-04-18T09:50:27.744+00:00", "id": 1519, "code": "DI", "setObjectB": [ 31 ] } ]
根据Jackson的JsonIdentityInfo文档说明:
In practice this is done by serializing the first instance as full object and object identity, and other references to the object as reference values.
需实现所有条目返回完整POJO,以下是可行解决办法:
解决办法
方法1:移除@JsonIdentityInfo注解
@JsonIdentityInfo的核心作用是处理循环引用时用ID替代重复对象。如果你的业务场景不存在循环引用风险(比如自引用不会形成闭环),直接删除ObjectB上的@JsonIdentityInfo和@JsonIdentityReference注解即可,Jackson会每次都序列化完整对象。
注意:若自引用存在循环(如某个对象的objectBRecursion指向自身或形成闭环),会触发StackOverflowError。
方法2:使用DTO层转换
避免直接序列化JPA实体,定义对应DTO类,在服务层将实体属性拷贝到DTO后再序列化,完全控制输出结构。
示例DTO定义:
public class ObjectADTO { private Long id; private String version; private String code; private Set<ObjectBDTO> setObjectB; // Getter、Setter及属性拷贝方法 } public class ObjectBDTO { private Long id; private String version; private String code; private ObjectBDTO objectBRecursion; // Getter、Setter及属性拷贝方法 }
在Service层查询实体后,使用MapStruct、ModelMapper等工具将实体拷贝到DTO,返回DTO给前端。这种方式隔离了实体层与表现层,灵活性最高。
方法3:自定义Jackson序列化器
为ObjectB编写自定义序列化器,强制每次序列化完整对象,忽略Jackson的身份识别机制。
示例序列化器:
public class ObjectBSerializer extends StdSerializer<ObjectB> { public ObjectBSerializer() { super(ObjectB.class); } @Override public void serialize(ObjectB value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeNumberField("id", value.getId()); gen.writeStringField("version", value.getVersion().toString()); gen.writeStringField("code", value.getCode()); if (value.getObjectBRecursion() != null) { gen.writeObjectField("objectBRecursion", value.getObjectBRecursion()); } gen.writeEndObject(); } }
在ObjectB实体上指定该序列化器:
@Entity @Table(name = "TABLE_OBJECT_B") @JsonSerialize(using = ObjectBSerializer.class) public class ObjectB { // 原有代码 }
注意:后续实体属性变更时需同步修改序列化器,维护成本较高。
方法4:通过Jackson配置禁用身份识别
不修改实体注解,通过配置ObjectMapper覆盖ObjectB的身份识别注解:
@Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 使用MixIn覆盖ObjectB的@JsonIdentityInfo注解 mapper.addMixIn(ObjectB.class, ObjectBMixIn.class); return mapper; } // 空的MixIn类,不添加@JsonIdentityInfo注解 abstract class ObjectBMixIn {} }
这种方式既保留其他Jackson注解的功能,又针对性禁用了ObjectB的身份识别逻辑。
内容的提问来源于stack exchange,提问作者Mike Wallace

