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

添加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()方法,但双向关联的实体在生成这些方法时会陷入循环引用:

  1. Kurs2Trainer的hashCode()会包含trainer和kurs字段(@Data默认包含所有字段)
  2. Kursuser的hashCode()会包含trainerBei集合,而集合内的元素是Kurs2Trainer对象
  3. 计算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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:55:54