Spring+Hibernate多对多REST接口JSON序列化异常及字段为空问题
咱们先把问题的来龙去脉理清楚:
最初的错误核心是懒加载集合在Session关闭后无法初始化:当Service层返回Supplier实体时,事务已经结束,Hibernate的Session被关闭了。Jackson序列化SupplierObject时,会遍历Ingredient对象,而Ingredient里的suppliers是懒加载集合,此时没有Session可以用来加载这个集合,所以抛出了初始化失败的异常。
用了懒加载序列化规避方案后,ingredients变成null的原因:你用的Jackson Hibernate模块默认会把未初始化的懒加载集合(比如Hibernate的PersistentBag)序列化为null,而非空集合。调试时能看到数据是因为IDE调试工具会调用集合的
toString()等方法,此时如果断点还在事务未结束的阶段(比如Service层),会触发懒加载,所以能看到数据,但实际序列化时Session已经关闭,集合未被初始化,就被Jackson处理成了null。
下面给你几个按推荐程度排序的解决方案:
方案一:用DTO隔离实体(最规范的长期方案)
直接把数据库实体暴露给前端会带来循环引用、懒加载、字段泄露等问题,用DTO(数据传输对象)隔离是最佳实践:
创建
IngredientObjectDTO,只保留前端需要的字段,不要包含suppliers集合(切断循环引用):@JsonIgnoreProperties(ignoreUnknown = true) public class IngredientObject extends IdAbstractObject { public String name; // 替换成你实际需要的字段 public String code; // 其他业务需要的字段... public IngredientObject() {} public IngredientObject(Ingredient ingredient) { this.id = ingredient.getId(); this.name = ingredient.getName(); this.code = ingredient.getCode(); // 赋值其他需要的字段 } }修改
SupplierObject,将List<Ingredient>替换为List<IngredientObject>,并在构造方法中完成实体到DTO的转换:@JsonIgnoreProperties(ignoreUnknown = true) public class SupplierObject extends IdAbstractObject { // 原有字段:email、phoneNumber等... public List<IngredientObject> ingredients = new ArrayList<>(); public SupplierObject(Supplier supplier) { // 原有字段赋值逻辑... id = supplier.getId(); email = supplier.getEmail(); responsible = supplier.getResponsible(); // ...其他字段 // 转换Ingredient实体为IngredientObject this.ingredients = supplier.getIngredients().stream() .map(IngredientObject::new) .collect(Collectors.toList()); } }
这个方案从根源上解决了循环引用和懒加载触发的问题,同时也能精准控制返回给前端的字段。
方案二:在Service层提前初始化集合(快速临时方案)
如果暂时不想修改DTO结构,可以利用事务在Session关闭前触发懒加载:
@Transactional // 确保方法在事务范围内,Session未关闭 public Supplier get(Long supplierId) { Supplier supplier = supplierDao.getById(supplierId); if (supplier == null) throw new ResourceNotFound("supplier", supplierId); // 调用size()触发ingredients集合的懒加载,此时Session还存在 supplier.getIngredients().size(); return supplier; }
更高效的方式是在DAO层使用JOIN FETCH查询,一次性加载关联的Ingredients:
// SupplierDao中的方法 public Supplier getByIdWithIngredients(Long id) { return entityManager.createQuery( "SELECT s FROM Supplier s JOIN FETCH s.ingredients WHERE s.id = :id", Supplier.class) .setParameter("id", id) .getSingleResult(); }
然后在Service层调用这个方法,集合会被立即加载,序列化时就不会有懒加载问题了。
方案三:配置Jackson Hibernate模块,序列化未初始化集合为空集合
如果你使用Spring Boot,可以自定义ObjectMapper,让Jackson把未初始化的懒加载集合转换成空集合:
@Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper(Hibernate5Module hibernate5Module) { ObjectMapper mapper = new ObjectMapper(); // 配置:将未初始化的持久化集合替换为空的普通集合 hibernate5Module.configure(Hibernate5Module.Feature.REPLACE_PERSISTENT_COLLECTIONS, true); mapper.registerModule(hibernate5Module); return mapper; } }
这个配置会让Jackson把未初始化的PersistentBag转换成空ArrayList,JSON中的ingredients字段就不会是null了。不过要注意,这种方式仍可能存在循环引用风险,所以还是优先推荐方案一。
内容的提问来源于stack exchange,提问作者user3336884

