使用MapStruct解决父DTO含自引用子对象的无限循环问题
问题背景
实体类定义如下:
@Entity @Table(name = "flows") public class Flow { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Long id; @Column(name = "flow_name") private String flowName; @OneToOne @JoinColumn(name = "folder_id", referencedColumnName = "id") private Folder folder; } @Entity @Table(name = "folders") public class Folder { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Long id; @Column(name = "internal_id", unique = true) private Long internalId; @Column(name = "parent_internal_id") private Long parentInternalId; @Column(name = "project_internal_id") private Long projectInternalId; @Column(name = "folder_name") private String folderName; @Column(name="root_flg") private Boolean rootFlg; @OneToMany(mappedBy = "parent") private List<Folder> children = new ArrayList<>(); @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name = "parent_id", referencedColumnName = "id") private Folder parent; @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "project_id", referencedColumnName = "id") private Project project; @OneToOne(mappedBy = "folder") private Flow flow; }
DTO类定义如下:
public class FlowDTO implements Serializable { private final Long id; private final String flowName; private final Long folderId; private final String folderName; private final Long parentId; private final String parentName; private final Long projectId; private final String projectName; } public class FolderDTO implements Serializable { private final Long id; private final Long internalId; private final Long parentInternalId; private final String folderName; private final Boolean rootFlg; private final Long parentId; private final String parentName; private final Long projectId; private final String projectName; private final List<FolderDTO> children; }
FlowMapper接口定义如下:
@Mapper(componentModel = "spring") public interface FlowMapper { @Mapping(target="folderId", source="folder.id") @Mapping(target="folderName", source="folder.folderName") @Mapping(target="parentId", source="folder.parent.id") @Mapping(target="parentName", source="folder.parent.folderName") @Mapping(target="projectId", source="folder.project.id") @Mapping(target="projectName", source = "folder.project.projectName") FlowDTO flowToFlowDTO(Flow flow); List<FlowDTO> flowsToFlowDTOs(List<Flow> flows); @InheritInverseConfiguration Flow flowDTOToFlow(FlowDTO flowDTO); List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs); }
问题描述
反向映射时MapStruct未正确将parentId映射到folder.parent.id,反而生成了无限循环的代码。生成的Mapper实现类中关键代码如下:
protected Folder flowDTOToFolder(FlowDTO flowDTO) { if ( flowDTO == null ) { return null; } Folder folder = new Folder(); folder.setParent( flowDTOToFolder( flowDTO ) ); // 此处引发无限循环 folder.setProject( flowDTOToProject( flowDTO ) ); folder.setId( flowDTO.getFolderId() ); folder.setFolderName( flowDTO.getFolderName() ); return folder; }
原本考虑用@BeforeMapping,想知道更简便的最佳实践方案。
补充尝试
曾尝试添加@Named方法,但未生效,报错信息:"The type of parameter "flowDTO" has no property named "parent". Please define the source property explicitly.",添加的代码如下:
@InheritInverseConfiguration @Mapping(target="folder.parent", qualifiedByName="parentFolder") Flow flowDTOToFlow(FlowDTO flowDTO); List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs); @Named("parentFolder") @Mapping(target = "id", source = "parentId") @Mapping(target="folderName", source = "parentName") Folder flowDTOToParentFolder(FlowDTO flowDTO);
解决方案
要解决这个问题,核心是明确告诉MapStruct反向映射时如何处理folder.parent,避免它自动调用递归的转换方法。以下是两种可靠的实现方式:
方式一:拆分转换逻辑,显式定义映射规则
修改flowDTOToFlow方法,不再完全依赖@InheritInverseConfiguration,而是拆分嵌套对象的转换逻辑,明确每个层级的映射规则:
@Mapper(componentModel = "spring") public interface FlowMapper { // 正向映射保持不变 @Mapping(target="folderId", source="folder.id") @Mapping(target="folderName", source="folder.folderName") @Mapping(target="parentId", source="folder.parent.id") @Mapping(target="parentName", source="folder.parent.folderName") @Mapping(target="projectId", source="folder.project.id") @Mapping(target="projectName", source = "folder.project.projectName") FlowDTO flowToFlowDTO(Flow flow); List<FlowDTO> flowsToFlowDTOs(List<Flow> flows); // 反向映射:显式定义整体规则 @Mapping(target="id", source="id") @Mapping(target="flowName", source="flowName") @Mapping(target="folder", source="flowDTO") Flow flowDTOToFlow(FlowDTO flowDTO); List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs); // 自定义Folder转换,指定parent的转换方法 @Mapping(target="id", source="folderId") @Mapping(target="folderName", source="folderName") @Mapping(target="parent", source="flowDTO", qualifiedByName = "parentFolder") @Mapping(target="project", source="flowDTO", qualifiedByName = "project") Folder flowDTOToFolder(FlowDTO flowDTO); // 单独定义parent的转换,仅映射必要字段 @Named("parentFolder") @Mapping(target="id", source="parentId") @Mapping(target="folderName", source="parentName") Folder flowDTOToFolderParent(FlowDTO flowDTO); // 单独定义project的转换 @Named("project") @Mapping(target="id", source="projectId") @Mapping(target="projectName", source="projectName") Project flowDTOToProject(FlowDTO flowDTO); }
这种方式通过拆分转换逻辑,让MapStruct按照指定的方法处理嵌套对象,不会自动生成递归调用,从根源避免无限循环。
方式二:忽略自动映射,手动设置parent
如果不想拆分过多方法,可以通过@Mapping(ignore=true)阻止MapStruct自动处理folder.parent,再用@BeforeMapping手动设置parent字段:
@Mapper(componentModel = "spring") public interface FlowMapper { // 正向映射保持不变 @Mapping(target="folderId", source="folder.id") @Mapping(target="folderName", source="folder.folderName") @Mapping(target="parentId", source="folder.parent.id") @Mapping(target="parentName", source="folder.parent.folderName") @Mapping(target="projectId", source="folder.project.id") @Mapping(target="projectName", source = "folder.project.projectName") FlowDTO flowToFlowDTO(Flow flow); List<FlowDTO> flowsToFlowDTOs(List<Flow> flows); // 反向映射:忽略parent的自动映射 @InheritInverseConfiguration @Mapping(target="folder.parent", ignore = true) Flow flowDTOToFlow(FlowDTO flowDTO); List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs); // 手动设置parent字段 @BeforeMapping default void setFolderParent(FlowDTO source, @MappingTarget Flow target) { if (source.getParentId() != null) { Folder parent = new Folder(); parent.setId(source.getParentId()); parent.setFolderName(source.getParentName()); target.getFolder().setParent(parent); } } // 自定义project转换 @Mapping(target="id", source="projectId") @Mapping(target="projectName", source="projectName") Project flowDTOToProject(FlowDTO flowDTO); }
这种方式通过手动干预嵌套对象的赋值,避免MapStruct自动生成递归逻辑,实现起来更简洁。
关键注意点
- 对于存在自关联的实体(如
Folder.parent),避免依赖MapStruct的自动推断转换逻辑,显式定义映射规则更可靠。 @InheritInverseConfiguration会反转正向的所有映射,但无法识别递归边界,因此需要手动排除或指定嵌套对象的转换方式。
内容的提问来源于stack exchange,提问作者xenitis
相关产品推荐
相关产品推荐

