
本文讲解如何在 Spring Boot 中通过 Jackson 注解解决双向关联导致的序列化问题,使 Loan(子实体)响应中正确包含其关联的 User(父实体)信息,避免空值或无限递归。
本文讲解如何在 spring boot 中通过 jackson 注解解决双向关联导致的序列化问题,使 `loan`(子实体)响应中正确包含其关联的 `user`(父实体)信息,避免空值或无限递归。
在 Spring Boot 的 REST API 开发中,当 Loan 与 User 构成双向 JPA 关系时,直接调用 loan.getUser() 并期望 JSON 响应中包含完整用户信息,往往失败——常见表现为 user 字段为 null 或整个响应因循环引用而抛出 JsonMappingException。根本原因在于:
- @JsonIgnore 同时加在双向关系两侧,彻底屏蔽了序列化;
- FetchType.LAZY 导致 user 在未初始化时为代理对象,脱离事务后无法加载;
- 缺乏 Jackson 对双向关系的语义指导,引发无限递归或字段忽略。
✅ 正确解法是 组合使用 @JsonManagedReference 与 @JsonBackReference,而非简单移除 @JsonIgnore:
- @JsonManagedReference 标记“正向引用”(如 User.loan 列表),该字段会被序列化;
- @JsonBackReference 标记“反向引用”(如 Loan.user),该字段不参与序列化,但可被 Java 代码正常访问(如 loan.getUser().getName());
- 二者配对使用,既打破 JSON 循环,又保留 Java 层的对象导航能力。
修改后的实体代码如下:
// User.java(父实体)
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long mobile;
private String name;
private String email;
@ToString.Exclude
@OneToMany(cascade = CascadeType.ALL, mappedBy = "user", orphanRemoval = true)
@JsonManagedReference // ✅ 允许序列化 loan 列表(仅当需要返回 User 时生效)
private List<Loan> loans = new ArrayList<>();
}// Loan.java(子实体)
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
public class Loan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String status;
@ToString.Exclude
@ManyToOne(fetch = FetchType.EAGER) // ⚠️ 关键:改为 EAGER 或确保事务内初始化
@JoinColumn(name = "user_id")
@JsonBackReference // ✅ 不序列化 user 字段,但 Java 层可安全访问
private User user;
}? 重要补充说明:
- @JsonBackReference 本身不解决懒加载问题。若 fetch = FetchType.LAZY,loan.getUser() 在 HTTP 响应序列化阶段仍可能为 null(因脱离 @Transactional 上下文)。推荐在查询时显式 JOIN FETCH,或改用 FetchType.EAGER(适用于关联必查场景);
- 若需在分页查询中一并加载 User,应优化 Repository 方法:
@Query("SELECT l FROM Loan l JOIN FETCH l.user WHERE l.staffId = :staffId AND l.date BETWEEN :start AND :end") Page<Loan> findByStaffIdAndDateBetween(@Param("staffId") Long staffId, @Param("start") LocalDate startDate, @Param("end") LocalDate endDate, Pageable pageable);- @JsonIdentityInfo 可保留对象引用标识,但在单向序列化(如只返回 Loan)时非必需,可酌情移除以简化配置。
最终,在 Controller 中无需手动调用 ele.setUser(ele.getUser())(该操作无效且冗余),直接返回 loanPage 即可:
@GetMapping("/loans")
public ResponseEntity<Page<Loan>> getLoans(
@RequestParam Long staffId,
@RequestParam String startDate,
@RequestParam String endDate,
@RequestParam int page,
@RequestParam int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Loan> loanPage = loanRepo.findByStaffIdAndDateBetween(staffId,
LocalDate.parse(startDate), LocalDate.parse(endDate), pageable);
return ResponseEntity.ok(loanPage);
}响应示例(精简):
{
"content": [{
"id": 101,
"status": "ACTIVE",
"user": { "id": 201, "name": "Alice", "email": "alice@example.com" }
}],
"totalElements": 1,
"pageNumber": 0
}至此,Loan 实体将稳定携带其 User 父实体的完整字段,兼顾性能、可维护性与 JSON 兼容性。


















