添加Trainer到Kurs触发StackOverflowError,求定位错误原因
为课程添加讲师时触发StackOverflowError的排查
尝试为Kurs(课程)添加Trainer(讲师)时触发了StackOverflowError,不清楚循环出现的位置及原因,相关代码如下:
Kurs实体类
@EqualsAndHashCode( callSuper = false ) @Entity @Data @NoArgsConstructor @AllArgsConstructor public class Kurs extends PanacheEntityBase implements Serializable { @Id @GeneratedValue( strategy = GenerationType.AUTO ) private Long id; @Basic @Column(name = "name", nullable = false) private String name; @OneToMany(mappedBy = "kurs", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Set<Kurs2Trainer> trainers = new HashSet<>(); public void addTrainer(Kursuser trainer) { // Create a Kurs2Trainer object Kurs2Trainer kurs2Trainer = new Kurs2Trainer(); kurs2Trainer.setTrainer(trainer); kurs2Trainer.setKurs(this); // Add the Kurs2Trainer object to the trainers set this.trainers.add(kurs2Trainer); // Add this Kurs to the trainer's set trainer.getTrainerBei().add(kurs2Trainer); } }
Kursuser实体类
@Entity @Data @EqualsAndHashCode( callSuper = false ) @NoArgsConstructor @AllArgsConstructor public class Kursuser extends PanacheEntityBase implements Serializable { @Id private Long id; @OneToOne( fetch = FetchType.LAZY ) @MapsId private User user; /* Diese Person ist Trainer an den folgenden Kursen */ @OneToMany( mappedBy = "trainer", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private Set<Kurs2Trainer> trainerBei = new HashSet<>(); }
Kurs2Trainer实体类
@Entity @Data @NoArgsConstructor @AllArgsConstructor public class Kurs2Trainer implements Serializable { @EmbeddedId private Kurs2TrainerID id = new Kurs2TrainerID(); @MapsId("trainerId") @ManyToOne( fetch = FetchType.LAZY ) @JoinColumn( name = "trainer_id" ) private Kursuser trainer; @MapsId("kursId") @ManyToOne( fetch = FetchType.LAZY ) @JoinColumn( name = "kurs_id" ) private Kurs kurs; }
Kurs2TrainerID类
@Data @Embeddable public class Kurs2TrainerID implements Serializable { @Column(name = "trainerId") private Long trainerId; @Column(name = "kursId") private Long kursId; }
调用代码
private void addTrainerToKurs(User trainer, Kurs k){ //Check if we have already the trainer in the course if ( CollectionUtils.isNotEmpty( k.getTrainers() ) ) { for(Kurs2Trainer kTrainer : k.getTrainers()){ if(kTrainer.getId().getTrainerId().equals( trainer.getId())){ return; } } } //We dont have this user in our course so add Kursuser trainerKursuser = kursuserService.getKursuser(trainer); k.addTrainer(trainerKursuser); }
错误栈
java.lang.StackOverflowError at at.tanzauswertung.portal.kurs.entities.Kursuser.$$_hibernate_read_id(Kursuser.java) at at.tanzauswertung.portal.kurs.entities.Kursuser.getId(Kursuser.java:25) at at.tanzauswertung.portal.kurs.entities.Kursuser.hashCode(Kursuser.java:19) at at.tanzauswertung.portal.kurs.entities.Kurs2Trainer.hashCode(Kursuser.java:11) at java.base/java.util.AbstractSet.hashCode(AbstractSet.java:124)
问题分析与解决方案
核心原因
你使用了Lombok的@Data注解,该注解会自动生成equals()和hashCode()方法,但双向关联的实体在生成这些方法时会陷入循环引用:
Kurs2Trainer的hashCode()会包含trainer和kurs字段(@Data默认包含所有字段)Kursuser的hashCode()会包含trainerBei集合,而集合内的元素是Kurs2Trainer对象- 计算
Kursuser的hashCode时,会遍历trainerBei集合计算每个Kurs2Trainer的hashCode,每个Kurs2Trainer又会反向计算Kursuser的hashCode,无限循环导致栈溢出
错误栈里的Kurs2Trainer.hashCode(Kursuser.java:11)是Lombok生成代码时的路径错误,但核心问题还是循环引用。
解决方案
方案1:修改Lombok注解,排除关联集合字段
在Kurs和Kursuser类上,用@EqualsAndHashCode手动排除关联集合字段,避免循环引用:
修改Kurs类:
@EqualsAndHashCode(callSuper = false, exclude = {"trainers"}) @Entity @Data @NoArgsConstructor @AllArgsConstructor public class Kurs extends PanacheEntityBase implements Serializable { // 原有代码不变 }
修改Kursuser类:
@EqualsAndHashCode(callSuper = false, exclude = {"trainerBei"}) @Entity @Data @NoArgsConstructor @AllArgsConstructor public class Kursuser extends PanacheEntityBase implements Serializable { // 原有代码不变 }
方案2:手动编写equals和hashCode方法
对于关联实体,仅使用主键字段生成equals()和hashCode(),既符合实体相等判断逻辑(主键相同则实体相同),又能避免循环引用。例如在Kursuser中手动编写:
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Kursuser kursuser = (Kursuser) o; return id != null && id.equals(kursuser.id); } @Override public int hashCode() { return getClass().hashCode(); }
同理,Kurs和Kurs2Trainer也可采用这种方式,Kurs2Trainer可基于嵌入式主键id生成equals和hashCode。
额外优化:维护关联关系时同步嵌入式主键
在Kurs.addTrainer方法中,设置Kurs2Trainer的trainer和kurs后,需同步更新嵌入式主键字段,避免主键不一致:
public void addTrainer(Kursuser trainer) { Kurs2Trainer kurs2Trainer = new Kurs2Trainer(); kurs2Trainer.setTrainer(trainer); kurs2Trainer.setKurs(this); // 同步嵌入式主键 kurs2Trainer.getId().setTrainerId(trainer.getId()); kurs2Trainer.getId().setKursId(this.getId()); this.trainers.add(kurs2Trainer); trainer.getTrainerBei().add(kurs2Trainer); }
内容的提问来源于stack exchange,提问作者Paul
相关产品推荐
相关产品推荐

