You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

双向关联实体调用toString触发java.lang.StackOverflowError问题求助

解决JPA双向关联实体toString循环调用导致的StackOverflowError

问题根源

你的JPA实体之间存在双向关联:

  • Person包含List<Tool>,每个Tool又引用Person
  • Brand包含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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 15:27:38