如何在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>
解决方案
这个错误的核心是双向关联导致的循环引用:
Schools包含Students集合,Students又引用了Schools,Jackson序列化时会陷入无限循环。- 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
相关产品推荐
相关产品推荐

