Spring Boot测试中JsonPath获取嵌套对象返回null的原因与解决
问题原因分析
你遇到的cliente返回null的问题,大概率和**JPA懒加载(Lazy Loading)**与Jackson序列化的交互有关,咱们拆解下核心原因:
JPA默认懒加载行为:
@ManyToOne注解默认的FetchType是LAZY,也就是说当你从数据库获取Referente对象时,它关联的Cliente并不会立即被加载,而是生成一个Hibernate代理对象。如果你的initializeData()方法执行完后Session已经关闭,此时调用referente1.getCliente()无法初始化这个代理对象,Jackson序列化时就会把它转成null。第二个测试正常的原因:你第二个测试是直接查询
Cliente实体,它关联的Agente虽然也是懒加载,但可能在初始化测试数据时,你主动获取过agentePrimoContatto(触发了代理初始化),或者查询Cliente的语句用了关联加载,所以序列化时能拿到正常的Agente对象。
最优解决方案推荐
下面是从优到次的解决方式,你可以根据项目阶段和需求选择:
1. 使用DTO(数据传输对象)返回接口结果(最推荐)
直接返回JPA实体到接口/测试场景是非常不规范的做法,容易引发懒加载问题、暴露敏感字段、耦合数据库结构和接口返回结构。正确的姿势是定义DTO类,在Service层完成实体到DTO的转换:
// 定义ReferenteDTO public class ReferenteDTO { private Integer idReferente; private ClienteDTO cliente; private String cognomeReferente; // 构造器、getter/setter } // 定义ClienteDTO public class ClienteDTO { private Integer idCliente; private String nominativo; private LocalDateTime dataProssimoContatto; private AgenteDTO agentePrimoContatto; // 构造器、getter/setter } // 定义AgenteDTO public class AgenteDTO { private Integer idAgente; private String nome; private String cognome; private String telefono; // 构造器、getter/setter }
在Service层做转换(可以用MapStruct这类工具简化重复代码):
public List<ReferenteDTO> getAllReferenti() { return referenteRepository.findAll().stream() .map(referente -> { ReferenteDTO dto = new ReferenteDTO(); dto.setIdReferente(referente.getIdReferente()); dto.setCognomeReferente(referente.getCognomeReferente()); // 手动初始化关联对象(或在查询时用JOIN FETCH批量加载) Cliente cliente = referente.getCliente(); ClienteDTO clienteDTO = new ClienteDTO(); clienteDTO.setIdCliente(cliente.getIdCliente()); clienteDTO.setNominativo(cliente.getNominativo()); clienteDTO.setDataProssimoContatto(cliente.getDataProssimoContatto()); Agente agente = cliente.getAgentePrimoContatto(); AgenteDTO agenteDTO = new AgenteDTO(); agenteDTO.setIdAgente(agente.getIdAgente()); agenteDTO.setNome(agente.getNome()); agenteDTO.setCognome(agente.getCognome()); agenteDTO.setTelefono(agente.getTelefono()); clienteDTO.setAgentePrimoContatto(agenteDTO); dto.setCliente(clienteDTO); return dto; }) .collect(Collectors.toList()); }
测试时直接断言DTO的字段即可:
@Test public void getAllReferente_Test() throws Exception { initializeData(); mvc.perform(get("/referente").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].idReferente").value(referente1.getIdReferente())) .andExpect(jsonPath("$[0].cliente.idCliente").value(referente1.getCliente().getIdCliente())) .andExpect(jsonPath("$[0].cliente.nominativo").value(referente1.getCliente().getNominativo())) .andExpect(jsonPath("$[0].cognomeReferente").value(referente1.getCognomeReferente())); }
2. 配置Jackson Hibernate模块处理懒加载
如果暂时不想重构为DTO,可以添加Jackson的Hibernate模块自动处理懒加载代理。首先添加Maven依赖:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-hibernate5</artifactId> </dependency>
然后配置Jackson注册该模块:
@Configuration public class JacksonConfig { @Bean public Module hibernateModule() { return new Hibernate5Module(); } }
这个模块会在序列化时自动初始化懒加载代理,但需要确保控制器方法处于@Transactional注解下(保证Session在序列化时未关闭)。
3. 测试中手动初始化关联对象
如果只是为了快速让测试通过,可以在initializeData()里手动触发代理初始化:
private void initializeData() { // 创建并保存Agente、Cliente、Referente的代码... // 初始化Referente的Cliente关联 Hibernate.initialize(referente1.getCliente()); // 若需要,继续初始化Cliente的Agente关联 Hibernate.initialize(referente1.getCliente().getAgentePrimoContatto()); }
注意:这种方法仅适合测试场景,线上环境使用会引发N+1查询性能问题。
4. 修改FetchType为EAGER(不推荐)
直接把@ManyToOne的加载策略改成EAGER:
@ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name="idCliente") private Cliente cliente;
这种方法虽然简单,但会导致每次查询Referente都强制加载Cliente及其关联对象,严重影响查询性能,生产环境绝对不推荐。
内容的提问来源于stack exchange,提问作者greatgonzoitaly

