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

如何在Thymeleaf的JavaScript中访问含集合对象的Spring Boot POJO

问题描述

我定义了Schools和Students两个实体类,二者是一对多关联(一个学校包含多个学生)。尝试通过Thymeleaf在JavaScript中访问控制器传递到页面的Schools列表时,控制台抛出Jackson序列化错误:

at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774) ~[jackson-databind-2.13.3.jar:2.13.3]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.13.3.jar:2.13.3]
    at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:145) ~[jackson-databind-2.13.3.jar:2.13.3]
    at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:107) ~[jackson-databind-2.13.3.jar:2.13.3]
    at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:25) ~[jackson-databind-2.13.3.jar:2.13.3]
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.13.3.jar:2.13.3]

排查后发现,只有在Thymeleaf的JavaScript中访问对象时才会报错,直接在HTML里用Thymeleaf访问数据完全正常。


Schools实体类代码

@Entity
public class Schools {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @NonNull
    private String schoolName;
    @NonNull
    private String type; 
    @NonNull
    private String schoolGrade; 
    @NonNull
    private int studentCount; 
    @NonNull
    private int startHour; 
    @NonNull 
    private int startMin; 
    @NonNull 
    private String startAmPm; 
    @NonNull 
    private int endHour; 
    @NonNull
    private int endMin; 
    @NonNull
    private String endAmPm;
    @NonNull
    private Date startDate; 
    @NonNull
    private Date endDate; 
    @NonNull
    private int schoolDays;
    @NonNull
    private String address;
    @NonNull
    private String city;
    @NonNull
    private String state;
    @NonNull
    private int zip;
    @NonNull
    private String phone;
    @NonNull
    private boolean isActive;
    
    // 一对一关联:每个学校对应一个位置点
    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "location_id", referencedColumnName = "location_id")
    private LocationPoint location;
    
    // 一对多关联:一个学校对应多个学生
    //@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval=true)
    @OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.REMOVE, CascadeType.REFRESH, CascadeType.DETACH}, orphanRemoval=true)
    @JoinColumn(name = "school_id")
    private Set<Students> students = new HashSet<>();
}

Students实体类代码

@Entity
public class Students {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int studentId;
    @NonNull private int studId;
    @NonNull private boolean graduated;
    @NonNull private String firstName;
    @NonNull private String midName;
    @NonNull private String lastName;
    @NonNull private String suffix;
    @NonNull private float grade;
    @NonNull private String address1;
    @Column(columnDefinition = "TEXT")
    @NonNull private String address2;
    @NonNull private String city;
    @NonNull private String state;
    @NonNull private int zipCode;
    @NonNull private String gender; 
    @NonNull private Date birthDate; 
    @NonNull private String phoneNum;
    @NonNull private String cellPhoneNum;    
    @NonNull private String emergencyPhoneNum;
    @NonNull private String parentName;
    @NonNull private String schoolName;
    @NonNull private Date enter;
    @NonNull private boolean isEnter;
    @NonNull private Date Withdrawl;
    @NonNull private boolean isWithdrawl;
    @NonNull private boolean hazardousRoad;
    @NonNull private String travelMode;
    @NonNull private String driverNote;
    @NonNull private String misc;
    @NonNull private String journal;
    @NonNull private String medical;
    @NonNull private boolean isCustody;
    @Column(columnDefinition = "TEXT") private String alt1Name;
    @Column(columnDefinition = "TEXT") private String alt1Relationship;
    @Column(columnDefinition = "TEXT") private String alt1Address1;
    @Column(columnDefinition = "TEXT") private String alt1Address2;
    @Column(columnDefinition = "TEXT") private String alt1City;
    @Column(columnDefinition = "TEXT") private String alt1State;
    @Column(columnDefinition = "TEXT") private long alt1ZipCode;
    @Column(columnDefinition = "TEXT") private String alt1Phone;
    @Column(columnDefinition = "TEXT") private String alt1CellPhone;
    @Column(columnDefinition = "TEXT") private String alt2Name;
    @Column(columnDefinition = "TEXT") private String alt2Relationship;
    @Column(columnDefinition = "TEXT") private String alt2Address1;
    @Column(columnDefinition = "TEXT") private String alt2Address2;
    @Column(columnDefinition = "TEXT") private String alt2City;
    @Column(columnDefinition = "TEXT") private String alt2State;
    @Column(columnDefinition = "TEXT") private long alt2ZipCode;
    @Column(columnDefinition = "TEXT") private String alt2Phone;
    @Column(columnDefinition = "TEXT") private String alt2CellPhone;
    @Column(columnDefinition = "TEXT") private String alt3Name;
    @Column(columnDefinition = "TEXT") private String alt3Relationship;
    @Column(columnDefinition = "TEXT") private String alt3Address1;
    @Column(columnDefinition = "TEXT") private String alt3Address2;
    @Column(columnDefinition = "TEXT") private String alt3City;
    @Column(columnDefinition = "TEXT") private String alt3State;
    @Column(columnDefinition = "TEXT") private long alt3ZipCode;
    @Column(columnDefinition = "TEXT") private String alt3Phone;
    @Column(columnDefinition = "TEXT") private String alt3CellPhone;
    @NonNull private String studentType1;
    @NonNull private String studentType2;
    @NonNull private String studentType3;
    @NonNull private String pickUpLocation;
    @NonNull private int pickUpStopNo;
    @NonNull private String busRoute1;
    @NonNull private String pickUpTime;
    @NonNull private String deliveryLocation;
    @NonNull private int deliveryStopNo;
    @NonNull private String busRoute2;
    @NonNull private String deliveryTime;
   
    // 多对一关联:多个学生对应一个学校
    @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private Schools school;
    
    // 多对一关联:多个学生对应一个接送点
    @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private PickupDropoff2 pickupDropoff;

    // 一对一关联:每个学生对应一个位置点
    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private LocationPoint location;
}

控制器代码

// 跳转至接送点创建页面
@RequestMapping({"/createPickupDropoff"})
public String createPickupDropoff(PickupDropoff2 pickupDropoff, Model model) {
    
    model.addAttribute("schools", schoolRepo.findAll());
    model.addAttribute("students", studentRepo.findAll());
    
    return "create-pickupDropoff-point";
}

JavaScript代码

<script type="text/javascript" th:inline="javascript">
  var schools = /*[[${schools}]]*/ 'false';
</script>

解决方案

这个错误的核心是双向关联导致的循环引用:

  1. Schools包含Students集合,Students又引用了Schools,Jackson序列化时会陷入无限循环。
  2. HTML中Thymeleaf直接渲染时只会用到表面属性,不会触发完整的关联对象序列化,所以不会报错;但JavaScript中要把整个对象序列化为JSON,就会暴露循环引用问题。

解决办法有三种:

方法1:用@JsonIgnore忽略反向关联

在Students实体的school字段上添加@JsonIgnore注解,阻止Jackson序列化这个反向关联:

@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JsonIgnore // 添加该注解
private Schools school;

方法2:使用@JsonManagedReference和@JsonBackReference标记双向关联

在Schools的students字段上标记@JsonManagedReference(主动序列化的一方),在Students的school字段上标记@JsonBackReference(被忽略的反向关联):

// Schools类中
@OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.REMOVE, CascadeType.REFRESH, CascadeType.DETACH}, orphanRemoval=true)
@JoinColumn(name = "school_id")
@JsonManagedReference // 添加该注解
private Set<Students> students = new HashSet<>();

// Students类中
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JsonBackReference // 添加该注解
private Schools school;

方法3:创建DTO(数据传输对象)

定义只包含前端需要的字段的DTO类,在控制器中把实体转换为DTO后再传递给页面,从根源避免循环引用和不必要的字段序列化:

// 示例SchoolDTO
public class SchoolDTO {
    private long id;
    private String schoolName;
    // 只保留前端需要的其他字段,不包含students集合或仅包含学生基础信息DTO
    
    // getter、setter、构造方法
}

// 控制器中修改
@RequestMapping({"/createPickupDropoff"})
public String createPickupDropoff(PickupDropoff2 pickupDropoff, Model model) {
    List<Schools> schools = schoolRepo.findAll();
    List<SchoolDTO> schoolDTOs = schools.stream()
        .map(school -> {
            SchoolDTO dto = new SchoolDTO();
            dto.setId(school.getId());
            dto.setSchoolName(school.getSchoolName());
            // 其他字段映射
            return dto;
        })
        .collect(Collectors.toList());
    model.addAttribute("schools", schoolDTOs);
    model.addAttribute("students", studentRepo.findAll());
    
    return "create-pickupDropoff-point";
}

推荐方法3,因为它更符合分层架构设计,避免实体类与序列化逻辑耦合,也能精准控制前端获取的数据。


内容的提问来源于stack exchange,提问作者Shane Smith

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 05:17:55