Spring Boot 2.3.x 自定义@RestController如何应用投影及返回HAL格式数据
问题场景
我尝试在标注了@RestController的自定义控制器返回的实体上应用投影功能,代码如下:
@RequestMapping(method = GET, value = "customer-api/students/viewProfile") public @ResponseBody ResponseEntity<?> fetchProfile(PersistentEntityResourceAssembler resourceAssembler) { Student student = studentRepo.findByCreatedBy(accessToken.getSubject()); if (student != null) { return new ResponseEntity<>(resourceAssembler.toModel(student), HttpStatus.OK); } else { return new ResponseEntity<>(null, HttpStatus.NOT_FOUND); } }
运行时抛出无限递归异常,报错信息如下:
org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.springframework.data.jpa.mapping.JpaPersistentEntityImpl["idProperty"]->org.springframework.data.jpa.mapping.JpaPersistentPropertyImpl["owner"]->org.springframework.data.jpa.mapping.JpaPersistentEntityImpl["idProperty"]->org.springframework.data.jpa.mapping.JpaPersistentPropertyImpl["owner"]
该代码在Spring Boot 1.5.17版本中运行正常,可以正常应用投影并返回HAL格式的JSON,但升级到Spring 2.3.x版本后就报错。需要实现Spring Data REST导出控制器的相关能力,包括投影功能、返回HAL格式JSON。
补充说明
Student实体不存在双向关联关系,同时使用了PersistentEntityResourceAssembler来组装Student实体的响应体,按照规范说明:
PersistentEntityResourceAssembler - 通常注入到控制器方法中,它会按照Spring Data REST的规范渲染单个实体,指向托管类型的关联会被渲染为链接等形式。
@ManyToOne关联会被渲染为链接,不符合递归问题出现的常规触发条件。
根因分析
Spring Boot 2.3+版本对Spring Data REST的序列化逻辑做了调整:
- 1.5.x版本中自定义控制器返回
PersistentEntityResource时,Spring会自动应用Spring Data REST内置的Jackson序列化器处理 - 2.3.x版本后该序列化器默认仅作用于Spring Data REST自动生成的接口,自定义控制器返回的
PersistentEntityResource会被默认Jackson序列化器处理,直接序列化对象内部的JPA元数据属性JpaPersistentEntityImpl和JpaPersistentPropertyImpl,两者互相持有对方引用触发无限递归。
解决方案
方案1:使用PersistentEntityResource.Builder手动构建返回资源(推荐)
主动指定投影参数、关联链接,保证序列化逻辑符合Spring Data REST规范:
// 所需依赖可以直接从Spring上下文注入 private final ProjectionFactory projectionFactory; private final PersistentEntity<Student, ?> persistentEntity; private final EntityLinks entityLinks; @RequestMapping(method = GET, value = "customer-api/students/viewProfile") public ResponseEntity<?> fetchProfile() { Student student = studentRepo.findByCreatedBy(accessToken.getSubject()); if (student == null) { return ResponseEntity.notFound().build(); } // 应用指定投影,替换为你自定义的投影接口即可 Object projectedStudent = projectionFactory.createProjection(StudentProfileProjection.class, student); // 构建符合Spring Data REST规范的资源对象 PersistentEntityResource resource = PersistentEntityResource.build(projectedStudent, persistentEntity) .withLink(entityLinks.linkToItemResource(Student.class, student.getId()).withSelfRel()) .build(); return ResponseEntity.ok(resource); }
方案2:复用Spring Data REST内置控制器逻辑
自定义控制器继承RepositoryEntityController,直接复用框架自带的序列化、投影、关联链接生成能力,无需手动处理资源组装逻辑。
方案3:手动注册全局序列化器
在配置类中注册Spring Data REST专属的Jackson模块,让自定义控制器返回的资源也能被正确序列化:
@Configuration public class CustomRestConfig { @Bean public Module persistentEntityJackson2Module(Associations associations, EntityLinks entityLinks, RelProvider relProvider, RepositoryInvokerFactory repositoryInvokerFactory, ProjectionFactory projectionFactory) { return new PersistentEntityJackson2Module(associations, entityLinks, relProvider, repositoryInvokerFactory, projectionFactory); } }
以上所有方案都可以在自定义控制器中实现投影功能、返回标准HAL格式JSON的需求。
内容的提问来源于stack exchange,提问作者Charlie

