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

MapStruct一对多映射失败:循环依赖与字段未映射问题求助

解决MapStruct循环引用+字段不映射问题

问题现象

  • 实体添加一对多关联后,Spring应用启动失败,提示StudentMapper与ProgramMapper存在循环引用
  • 引入CycleAvoidingMappingContext后仍无法启动,怀疑Lombok的@Builder注解导致,尝试禁用builder配置无效
  • 移除所有类的@Builder后应用可启动,但MapStruct生成的映射类仅通过空构造方法创建对象,所有字段值为null,未执行任何属性赋值

相关依赖

<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>1.6.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct-processor</artifactId>
   <version>1.6.3</version>
</dependency>

相关代码

Program实体类

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
@Entity
public class Program {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    @OneToMany(mappedBy = "enrolledProgram", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Student> enrolledStudents;
}

ProgramDTO类

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ProgramDTO {
    private Long id;
    @NotNull
    private String name;
    @NotNull
    private Set<StudentDTO> enrolledStudents;
}

Student实体类

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    @ManyToOne
    @JoinColumn(name = "program_id")
    private Program enrolledProgram;
}

StudentDTO类

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class StudentDTO {
    private Long id;
    @NotNull
    private String name;
    @NotNull
    private ProgramDTO enrolledProgram;
}

CycleAvoidingMappingContext类

public class CycleAvoidingMappingContext {
    private Map<Object, Object> knownInstances = new IdentityHashMap<>();
    @BeforeMapping
    public <T> T getMappedInstance(Object source, @TargetType Class<T> targetType) {
        return (T) knownInstances.get(source);
    }
    @BeforeMapping
    public void storeMappedInstance(Object source, @MappingTarget Object target) {
        knownInstances.put(source, target);
    }
}

ProgramMapper接口

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        injectionStrategy = InjectionStrategy.CONSTRUCTOR,
        uses = {StudentMapper.class}
)
public interface ProgramMapper {
    Program toProgram(ProgramDTO programDTO, @Context CycleAvoidingMappingContext context);
    @InheritInverseConfiguration
    ProgramDTO toProgramDTO(Program program, @Context CycleAvoidingMappingContext context);
}

StudentMapper接口

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        injectionStrategy = InjectionStrategy.CONSTRUCTOR,
        uses = {ProgramMapper.class}
)
public interface StudentMapper {
    Student toStudent(StudentDTO studentDTO, @Context CycleAvoidingMappingContext context);
    @InheritInverseConfiguration
    StudentDTO toStudentDTO(Student student, @Context CycleAvoidingMappingContext context);
}

生成的ProgramMapperImpl示例

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2025-05-27T08:09:19-0600",
    comments = "version: 1.6.3, compiler: javac, environment: Java 24 (Oracle Corporation)"
)
@Component
public class ProgramMapperImpl implements ProgramMapper {

    @Override
    public Program toProgram(ProgramDTO programDTO, CycleAvoidingMappingContext context) {
        Program target = context.getMappedInstance( programDTO, Program.class );
        if ( target != null ) {
            return target;
        }
        if ( programDTO == null ) {
            return null;
        }
        Program program = new Program();
        context.storeMappedInstance( programDTO, program );
        return program;
    }

    @Override
    public ProgramDTO toProgramDTO(Program program, CycleAvoidingMappingContext context) {
        ProgramDTO target = context.getMappedInstance( program, ProgramDTO.class );
        if ( target != null ) {
            return target;
        }
        if ( program == null ) {
            return null;
        }
        ProgramDTO programDTO = new ProgramDTO();
        context.storeMappedInstance( program, programDTO );
        return programDTO;
    }
}

解决方案

1. 解决循环引用问题

循环引用的核心原因是两个Mapper通过uses互相依赖,且使用了构造器注入策略,Spring无法处理循环依赖的构造器注入。

修改方案:
将两个Mapper的injectionStrategy改为InjectionStrategy.FIELD,让Spring通过字段注入解决循环依赖,同时保留CycleAvoidingMappingContext处理对象映射时的循环引用(防止栈溢出)。

修改后的ProgramMapper:

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        injectionStrategy = InjectionStrategy.FIELD,
        uses = {StudentMapper.class}
)
public interface ProgramMapper {
    Program toProgram(ProgramDTO programDTO, @Context CycleAvoidingMappingContext context);
    @InheritInverseConfiguration
    ProgramDTO toProgramDTO(Program program, @Context CycleAvoidingMappingContext context);
}

修改后的StudentMapper:

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        injectionStrategy = InjectionStrategy.FIELD,
        uses = {ProgramMapper.class}
)
public interface StudentMapper {
    Student toStudent(StudentDTO studentDTO, @Context CycleAvoidingMappingContext context);
    @InheritInverseConfiguration
    StudentDTO toStudentDTO(Student student, @Context CycleAvoidingMappingContext context);
}

2. 解决字段不映射问题

去掉@Builder后字段不映射,是因为MapStruct处理带@Context参数的方法时,需要显式声明映射关系,或者确保字段名称完全匹配。同时关联集合的映射需要明确指定使用对应Mapper的方法。

修改Mapper接口,显式声明字段映射:

修改后的ProgramMapper:

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        injectionStrategy = InjectionStrategy.FIELD,
        uses = {StudentMapper.class}
)
public interface ProgramMapper {
    @Mapping(target = "enrolledStudents", source = "enrolledStudents")
    Program toProgram(ProgramDTO programDTO, @Context CycleAvoidingMappingContext context);

    @InheritInverseConfiguration
    @Mapping(target = "enrolledStudents", source = "enrolledStudents")
    ProgramDTO toProgramDTO(Program program, @Context CycleAvoidingMappingContext context);
}

修改后的StudentMapper:

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        injectionStrategy = InjectionStrategy.FIELD,
        uses = {ProgramMapper.class}
)
public interface StudentMapper {
    @Mapping(target = "enrolledProgram", source = "enrolledProgram")
    Student toStudent(StudentDTO studentDTO, @Context CycleAvoidingMappingContext context);

    @InheritInverseConfiguration
    StudentDTO toStudentDTO(Student student, @Context CycleAvoidingMappingContext context);
}

如果要保留@Builder,无需额外配置,MapStruct会自动识别构建器,只要确保上述循环引用的解决方案已生效即可。

3. 验证步骤

  • 清理项目编译目录(如target),重新编译,确保MapStruct生成最新的实现类
  • 启动Spring应用,检查是否再出现循环引用异常
  • 编写测试代码,验证实体与DTO之间的映射是否正确,字段值是否正常赋值

内容的提问来源于stack exchange,提问作者Michael Millar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:12:06