Spring Boot Bean循环依赖问题求助(附相关代码示例)
解决Spring Bean循环依赖问题
问题背景
我正在开发一个调度项目,启动时提示Bean之间存在循环依赖错误。
相关代码
RoomController.java
@RestController @RequestMapping("/api/v1/rooms") public class RoomController { private final RoomService roomService; public RoomController(RoomService roomService) { this.roomService = roomService; } @GetMapping public ResponseEntity<List<RoomDto>> getAllRooms() { List<RoomDto> rooms = roomService.findAll(); return ResponseEntity .status(HttpStatus.OK) .body(rooms); } @PostMapping public ResponseEntity<RoomDto> createRoom(@Valid @RequestBody CreateRoomDto createRoomDto) { RoomDto roomDto = roomService.save(createRoomDto); return ResponseEntity .status(HttpStatus.CREATED) .body(roomDto); } @DeleteMapping("/{id}") public void deleteRoomById(@PathVariable Long id) { roomService.deleteById(id); } }
RoomService.java
@Service @Transactional public class RoomServiceImpl implements RoomService { private final RoomRepository roomRepository; private final RoomMapper roomMapper; public RoomServiceImpl(RoomRepository roomRepository, RoomMapper roomMapper) { this.roomRepository = roomRepository; this.roomMapper = roomMapper; } @Override public RoomDto save(CreateRoomDto createRoomDto) { Room room = roomMapper.toRoom(createRoomDto); return roomMapper.toRoomDto(roomRepository.save(room)); } @Override public List<RoomDto> findAll() { return roomRepository.findAll().stream().map(roomMapper::toRoomDto).collect(Collectors.toList()); } @Override public RoomDto findById(Long id) { Room room = roomRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Room with id " + id + " not found!")); return roomMapper.toRoomDto(room); } @Override public void deleteById(Long id) { roomRepository.deleteById(id); } }
RoomMapper.java
@Service public class RoomMapperImpl implements RoomMapper { private final LectureMapper lectureMapper; public RoomMapperImpl(LectureMapper lectureMapper) { this.lectureMapper = lectureMapper; } @Override public RoomDto toRoomDto(Room room) { return RoomDto.builder() .id(room.getId()) .name(room.getName()) .description(room.getDescription()) .number(room.getNumber()) .lectures(room.getLectures().stream().map( lectureMapper::toLectureDto ).collect(Collectors.toList())) .build(); } @Override public Room toRoom(CreateRoomDto createRoomDto) { return Room.builder() .name(createRoomDto.getName()) .description(createRoomDto.getDescription()) .number(createRoomDto.getNumber()) .build(); } @Override public RoomSmallDto toRoomSmallDto(Room room) { return RoomSmallDto.builder() .id(room.getId()) .name(room.getName()) .description(room.getDescription()) .number(room.getNumber()) .build(); } }
LectureMapper.java
@Service public class LectureMapperImpl implements LectureMapper { private final CohortMapper cohortMapper; private final InstructorMapper instructorMapper; private final RoomMapper roomMapper; public LectureMapperImpl(CohortMapper cohortMapper, InstructorMapper instructorMapper, RoomMapper roomMapper) { this.cohortMapper = cohortMapper; this.instructorMapper = instructorMapper; this.roomMapper = roomMapper; } @Override public LectureDto toLectureDto(Lecture lecture) { return LectureDto.builder() .id(lecture.getId()) .title(lecture.getTitle()) .description(lecture.getDescription()) .startDate(lecture.getStartDate()) .endDate(lecture.getEndDate()) .cohort(cohortMapper.toCohortSmallDto(lecture.getCohort())) .instructor(instructorMapper.toInstructorSmallDto(lecture.getInstructor())) .room(roomMapper.toRoomSmallDto(lecture.getRoom())) .build(); } }
CohortMapper.java
@Service public class CohortMapperImpl implements CohortMapper { private final LectureMapper lectureMapper; public CohortMapperImpl(LectureMapper lectureMapper) { this.lectureMapper = lectureMapper; } @Override public CohortDto toCohortDto(Cohort cohort) { return CohortDto.builder() .id(cohort.getId()) .major(cohort.getMajor()) .year(cohort.getYear()) .lectures(cohort.getLectures().stream().map( lectureMapper::toLectureDto ).collect(Collectors.toList())) .build(); } @Override public CohortSmallDto toCohortSmallDto(Cohort cohort) { return CohortSmallDto.builder() .id(cohort.getId()) .major(cohort.getMajor()) .year(cohort.getYear()) .build(); } }
实体类
@Data @Builder public class CohortSmallDto { private Long id; private String major; private int year; } --------------------------------------- @Data @Builder public class CohortDto { private Long id; private String major; private int year; private List<LectureDto> lectures; } --------------------------------------- @Data @Builder public class LectureDto { private Long id; private String title; private String description; private LocalDateTime startDate; private LocalDateTime endDate; private CohortSmallDto cohort; private InstructorSmallDto instructor; private RoomSmallDto room; }
循环依赖分析
代码中存在两条循环依赖链:
- RoomMapperImpl → LectureMapperImpl → RoomMapperImpl:RoomMapper需要LectureMapper转换RoomDto中的lectures列表,而LectureMapper又需要RoomMapper转换LectureDto中的room字段。
- LectureMapperImpl → CohortMapperImpl → LectureMapperImpl:CohortMapper需要LectureMapper转换CohortDto中的lectures列表,而LectureMapper又需要CohortMapper转换LectureDto中的cohort字段。
解决方案
方案1:使用@Lazy延迟初始化
在循环依赖的其中一个Bean的构造函数参数上添加@Lazy注解,Spring会创建代理对象延迟初始化依赖,避免启动时的循环问题。比如修改RoomMapperImpl的构造函数:
public RoomMapperImpl(@Lazy LectureMapper lectureMapper) { this.lectureMapper = lectureMapper; }
或者在LectureMapperImpl的RoomMapper参数上添加@Lazy,任选一处即可打破第一条循环链;同理,在CohortMapperImpl的LectureMapper参数上加@Lazy可打破第二条循环链。
方案2:重构代码打破循环
观察Dto转换逻辑,循环依赖是因为全量Dto转换时互相引用,但部分场景只需要小型Dto(比如RoomSmallDto、CohortSmallDto),可以针对性拆分:
- 打破RoomMapper与LectureMapper的循环:
LectureMapper转换LectureDto时只需要RoomSmallDto,而RoomSmallDto的转换不需要依赖LectureMapper,因此可以直接在LectureMapper中手动构建RoomSmallDto,不再依赖RoomMapper:
@Override public LectureDto toLectureDto(Lecture lecture) { RoomSmallDto roomSmallDto = null; if (lecture.getRoom() != null) { roomSmallDto = RoomSmallDto.builder() .id(lecture.getRoom().getId()) .name(lecture.getRoom().getName()) .description(lecture.getRoom().getDescription()) .number(lecture.getRoom().getNumber()) .build(); } return LectureDto.builder() .id(lecture.getId()) .title(lecture.getTitle()) .description(lecture.getDescription()) .startDate(lecture.getStartDate()) .endDate(lecture.getEndDate()) .cohort(cohortMapper.toCohortSmallDto(lecture.getCohort())) .instructor(instructorMapper.toInstructorSmallDto(lecture.getInstructor())) .room(roomSmallDto) .build(); }
修改后,LectureMapper不再依赖RoomMapper,第一条循环链被打破。
- 打破CohortMapper与LectureMapper的循环:
CohortMapper只有转换CohortDto时需要LectureMapper,而LectureMapper转换LectureDto时只需要CohortSmallDto(不需要LectureMapper)。可以把CohortMapper拆分为两个独立的Mapper:
CohortSmallMapper:只负责转换CohortSmallDto,不依赖任何其他Mapper;CohortFullMapper:负责转换包含lectures列表的CohortDto,依赖LectureMapper。
然后LectureMapper依赖CohortSmallMapper而非原CohortMapper,即可打破循环。
方案3:使用Setter注入替代构造函数注入
Spring默认支持Setter注入解决循环依赖,将其中一个Mapper的构造函数注入改为Setter注入。比如修改RoomMapperImpl:
@Service public class RoomMapperImpl implements RoomMapper { private LectureMapper lectureMapper; @Autowired public void setLectureMapper(LectureMapper lectureMapper) { this.lectureMapper = lectureMapper; } // 其他方法保持不变 }
Spring会先创建RoomMapperImpl实例,再注入LectureMapper,避免启动时的循环问题。同理可处理其他循环链。
内容的提问来源于stack exchange,提问作者user16073989
相关产品推荐
相关产品推荐

