双向关联实体调用toString触发java.lang.StackOverflowError问题求助
解决JPA双向关联实体toString循环调用导致的StackOverflowError
问题根源
你的JPA实体之间存在双向关联:
Person包含List<Tool>,每个Tool又引用PersonBrand包含List<Tool>,每个Tool又引用Brand
当调用toString()时,会陷入无限循环:Person.toString()调用toolsList.toString(),每个Tool.toString()又调用Person.toString(),反复递归直到栈内存耗尽,触发StackOverflowError。你手动编写的toString仍包含toolsList.toString(),所以问题未解决。
解决方案
1. 手动编写toString时规避关联循环
修改实体的toString方法,要么排除关联字段,要么只打印关联实体的ID/数量,避免触发递归:
Person类
@Override public String toString() { return "Person{" + "id=" + id + ", username='" + username + '\'' + ", role='" + role + '\'' + // 建议隐藏password等敏感字段 ", toolsCount=" + (toolsList != null ? toolsList.size() : 0) + '}'; }
Tool类
@Override public String toString() { return "Tool{" + "id=" + id + ", tool_name='" + tool_name + '\'' + ", cost=" + cost + ", personId=" + (person != null ? person.getId() : null) + ", brandId=" + (brand != null ? brand.getId() : null) + '}'; }
Brand类
@Override public String toString() { return "Brand{" + "id=" + id + ", brandName='" + brandName + '\'' + ", toolCount=" + (toolList != null ? toolList.size() : 0) + '}'; }
2. 使用Lombok的@ToString.Exclude注解
如果想继续用Lombok生成toString,给双向关联的字段添加@ToString.Exclude,让Lombok生成的方法跳过这些字段:
Person类
@Entity @Table(name = "person") @Getter @Setter @NoArgsConstructor @ToString public class Person { // ...其他字段 @OneToMany(mappedBy = "person") @ToString.Exclude private List<Tool> toolsList; }
Tool类
@Entity @Table(name = "tool") @Getter @Setter @NoArgsConstructor @ToString public class Tool { // ...其他字段 @ManyToOne @JoinColumn(name = "person_id", referencedColumnName = "id") @ToString.Exclude private Person person; @ManyToOne @JoinColumn(name = "brand_id", referencedColumnName = "id") @ToString.Exclude private Brand brand; }
Brand类
@Entity @Table(name = "brand") @Getter @Setter @NoArgsConstructor @ToString public class Brand { // ...其他字段 @OneToMany(mappedBy = "brand") @ToString.Exclude private List<Tool> toolList; }
3. 推荐方案:使用DTO(数据传输对象)
接口返回数据时,不要直接用JPA实体,而是定义DTO类,只保留需要展示的字段,从根源避免关联循环,同时保护敏感数据:
定义DTO类
// PersonDTO public class PersonDTO { private int id; private String username; private String role; private List<ToolDTO> tools; // 构造器、Getter、Setter(可使用Lombok的@Data) } // ToolDTO public class ToolDTO { private int id; private String tool_name; private int cost; private int personId; private int brandId; // 构造器、Getter、Setter } // BrandDTO public class BrandDTO { private int id; private String brandName; private List<ToolDTO> tools; // 构造器、Getter、Setter }
接口层转换实体为DTO
@GetMapping("/report") public ResponseEntity<?> getReport() { // 转换Person为PersonDTO List<PersonDTO> personDTOs = personService.getPersons().stream() .map(person -> { PersonDTO dto = new PersonDTO(); dto.setId(person.getId()); dto.setUsername(person.getUsername()); dto.setRole(person.getRole()); List<ToolDTO> toolDTOs = person.getToolsList().stream() .map(tool -> { ToolDTO toolDto = new ToolDTO(); toolDto.setId(tool.getId()); toolDto.setTool_name(tool.getTool_name()); toolDto.setCost(tool.getCost()); toolDto.setPersonId(tool.getPerson().getId()); toolDto.setBrandId(tool.getBrand().getId()); return toolDto; }).collect(Collectors.toList()); dto.setTools(toolDTOs); return dto; }).collect(Collectors.toList()); // 同理转换Brand和Tool为对应的DTO... List<BrandDTO> brandDTOs = brandService.getBrands().stream() .map(brand -> { BrandDTO dto = new BrandDTO(); dto.setId(brand.getId()); dto.setBrandName(brand.getBrandName()); List<ToolDTO> toolDTOs = brand.getToolList().stream() .map(tool -> { ToolDTO toolDto = new ToolDTO(); toolDto.setId(tool.getId()); toolDto.setTool_name(tool.getTool_name()); toolDto.setCost(tool.getCost()); toolDto.setPersonId(tool.getPerson().getId()); toolDto.setBrandId(tool.getBrand().getId()); return toolDto; }).collect(Collectors.toList()); dto.setTools(toolDTOs); return dto; }).collect(Collectors.toList()); List<ToolDTO> toolDTOs = toolService.getTools().stream() .map(tool -> { ToolDTO toolDto = new ToolDTO(); toolDto.setId(tool.getId()); toolDto.setTool_name(tool.getTool_name()); toolDto.setCost(tool.getCost()); toolDto.setPersonId(tool.getPerson().getId()); toolDto.setBrandId(tool.getBrand().getId()); return toolDto; }).collect(Collectors.toList()); return new ResponseEntity<>(new BodyResponse(HttpStatus.OK.toString(), "People report: " + personDTOs + "\n" + "Brand report: " + brandDTOs + "\n" + "Instrument report: " + toolDTOs), HttpStatus.OK); }
这种方式既解决了循环调用问题,又符合REST接口的设计规范,避免暴露不必要的实体字段。
内容的提问来源于stack exchange,提问作者Anna Kakanka
相关产品推荐
相关产品推荐

