Spring Boot查询Person集合时JSON序列化懒加载异常问题
这个问题的核心原因很明确:你在searchByExpression方法里手动关闭了Hibernate Session,而Person实体的otherNames等@OneToMany关联默认是**懒加载(FetchType.LAZY)**的。当Spring MVC试图把查询到的Person列表序列化成JSON时,需要访问这些懒加载集合,但此时Session已经关闭,无法初始化代理对象,于是抛出了failed to lazily initialize a collection of role... no Session的异常。而通过ID查询正常,是因为Spring Data JPA的findOne方法默认在事务上下文里执行,Session会保持到方法结束,序列化时可以正常初始化关联。
下面给你几种可行的解决方案,按推荐程度排序:
1. 移除手动关闭Session的代码,添加事务注解
你手动调用session.close()是错误的——EntityManager是由Spring容器管理的,Session的生命周期应该交给Spring来控制。给你的服务层方法添加@Transactional注解,让Spring在方法执行完毕后自动关闭Session,同时确保序列化时Session仍然可用:
修改你的服务层代码:
import org.springframework.transaction.annotation.Transactional; @SuppressWarnings("unchecked") @Override @Transactional // 添加事务注解 public List<Person> searchByExpression(List<Criterion> expressions) { Session session = entityManager.unwrap(Session.class); List<Person> persons = null; try { Criteria criteria = session.createCriteria(Person.class); for (Criterion simpleExpression : expressions) { criteria.add(simpleExpression); } persons = criteria.list(); }catch (Exception e) { e.printStackTrace(); } // 移除这行手动关闭Session的代码:session.close(); return persons; }
2. 在Criteria查询中主动抓取关联集合
如果不想依赖事务的Session生命周期,可以在查询时就主动加载需要的关联集合,这样即使Session关闭,集合已经被初始化了。用Criteria的setFetchMode或者createAlias结合fetch来实现:
import org.hibernate.FetchMode; @SuppressWarnings("unchecked") @Override public List<Person> searchByExpression(List<Criterion> expressions) { Session session = entityManager.unwrap(Session.class); List<Person> persons = null; try { Criteria criteria = session.createCriteria(Person.class); // 主动抓取otherNames集合,使用JOIN方式 criteria.setFetchMode("otherNames", FetchMode.JOIN); // 如果还有photos、travelDocuments需要序列化,也可以加上 // criteria.setFetchMode("photos", FetchMode.JOIN); // criteria.setFetchMode("travelDocuments", FetchMode.JOIN); // 加入去重,避免JOIN导致的重复结果 criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); for (Criterion simpleExpression : expressions) { criteria.add(simpleExpression); } persons = criteria.list(); }catch (Exception e) { e.printStackTrace(); } session.close(); return persons; }
3. 修改关联的FetchType为EAGER
直接把@OneToMany注解的fetch属性改成EAGER,这样每次查询Person时都会自动加载关联集合。这种方式最简单,但要注意性能问题——如果Person的关联集合数据量很大,会导致查询变慢:
修改Person实体的关联字段:
@OneToMany(mappedBy="person", fetch = FetchType.EAGER) // 添加fetch属性 private List<PersonOtherName> otherNames; @OneToMany(mappedBy="person", fetch = FetchType.EAGER) private List<Photo> photos; @OneToMany(mappedBy="person", fetch = FetchType.EAGER) private List<TravelDocument> travelDocuments;
4. 使用DTO(数据传输对象)封装返回结果
创建一个PersonDTO类,只包含你需要返回给前端的字段,在服务层把Person实体转换成DTO,这样序列化时不会访问懒加载的集合。这种方式最推荐,因为可以精准控制返回的数据,避免不必要的关联加载:
首先创建PersonDTO(根据你的实际字段调整):
import java.util.List; import java.util.stream.Collectors; public class PersonDTO { private Long id; private String name; private String surname; // 如果需要返回otherNames,创建对应的DTO private List<PersonOtherNameDTO> otherNames; // 构造方法 public PersonDTO(Person person) { this.id = person.getId(); this.name = person.getName(); this.surname = person.getSurname(); // 转换关联集合为DTO this.otherNames = person.getOtherNames().stream() .map(PersonOtherNameDTO::new) .collect(Collectors.toList()); } // 对应的PersonOtherNameDTO public static class PersonOtherNameDTO { private String name; // 其他需要的字段 public PersonOtherNameDTO(PersonOtherName otherName) { this.name = otherName.getName(); } // getter方法 } // getter方法 }
然后修改服务层方法返回DTO列表:
@Override public List<PersonDTO> searchByExpression(List<Criterion> expressions) { Session session = entityManager.unwrap(Session.class); List<Person> persons = null; try { Criteria criteria = session.createCriteria(Person.class); for (Criterion simpleExpression : expressions) { criteria.add(simpleExpression); } persons = criteria.list(); }catch (Exception e) { e.printStackTrace(); } session.close(); // 转换为DTO return persons.stream().map(PersonDTO::new).collect(Collectors.toList()); }
最后修改控制器的返回类型为ResponseEntity<List<PersonDTO>>。
5. 启用Open Session in View模式
在Spring Boot的application.properties里添加配置:
spring.jpa.open-in-view=true
这个配置会让Hibernate Session保持到视图渲染完成(也就是JSON序列化完成),这样即使Session在服务层关闭,序列化时仍然可以初始化懒加载集合。但这种方式不推荐用于高并发场景,因为Session会持有数据库连接更长时间,可能导致连接池耗尽。
内容的提问来源于stack exchange,提问作者Neritan Milaqi

