Spring Envers修订记录适配HATEOAS时遇PersistentEntity为空异常求助
场景复现
我正在使用Spring Boot、Spring Data REST、Spring Data Envers和Hibernate,想要通过Spring Data Envers加载实体的所有修订记录,并返回符合HATEOAS格式的响应。我的控制器代码如下:
@GetMapping(path = "/fines/{id}/revisions") public ResponseEntity<?> findRevisions(@PathVariable(value = "id") Long id, Pageable pageable, PersistentEntityResourceAssembler resourceAssembler) { if (id != null) { // 查询当前对象的所有修订记录 Page<Revision<Integer, Fine>> revisions = fineRepository.findRevisions(id, pageable); return new ResponseEntity<>(pagedResourcesAssembler.toResource(revisions, resourceAssembler), HttpStatus.OK); } else { throw new ResourceNotFoundException(); } }
遇到的问题
当我尝试用pagedResourcesAssembler.toResource(revisions, resourceAssembler)将修订记录转换为HATEOAS格式响应时,触发了以下异常:
19/01/2018 12:20:40,658 ERROR http-nio-8080-exec-4 [dispatcherServlet]:181 - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: PersistentEntity must not be null!]
如果直接返回revisions的话可以正常运行,但响应不符合HATEOAS规范,想知道哪里操作错了?
这个问题的核心原因很明确:Spring Data REST的PersistentEntityResourceAssembler无法识别Revision类型。Revision是Spring Data Envers提供的包装类,不是你自己定义的JPA实体,Spring Data REST没有为它注册对应的PersistentEntity元数据,所以在尝试转换时会抛出"PersistentEntity must not be null"的异常。
下面是两种可行的解决方式,推荐第一种,更直观易维护:
方式一:自定义HATEOAS资源包装类
我们可以手动把Revision对象包装成Spring HATEOAS能识别的ResourceSupport子类,再进行转换:
1. 创建自定义修订资源类
import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.mvc.ControllerLinkBuilder; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; public class RevisionResource extends ResourceSupport { private Integer revisionNumber; private LocalDateTime revisionDate; private Fine fine; // 构造器,直接从Revision对象提取数据 public RevisionResource(Revision<Integer, Fine> revision) { this.revisionNumber = revision.getRevisionNumber(); this.revisionDate = revision.getRevisionDate(); this.fine = revision.getEntity(); // 添加指向对应Fine实体的链接 add(linkTo(methodOn(FineController.class).getFine(fine.getId())).withRel("fine")); // 添加指向当前修订列表的链接 add(linkTo(methodOn(FineController.class).findRevisions(fine.getId(), null)).withRel("revisions")); } // 省略getter方法 public Integer getRevisionNumber() { return revisionNumber; } public LocalDateTime getRevisionDate() { return revisionDate; } public Fine getFine() { return fine; } }
2. 修改控制器方法转换资源
@GetMapping(path = "/fines/{id}/revisions") public ResponseEntity<?> findRevisions(@PathVariable(value = "id") Long id, Pageable pageable, PagedResourcesAssembler<RevisionResource> pagedResourcesAssembler) { if (id == null) { throw new ResourceNotFoundException(); } Page<Revision<Integer, Fine>> revisions = fineRepository.findRevisions(id, pageable); // 将Revision列表转换为自定义的RevisionResource列表 Page<RevisionResource> revisionResources = revisions.map(RevisionResource::new); // 现在可以正常转换为HATEOAS分页资源 return ResponseEntity.ok(pagedResourcesAssembler.toResource(revisionResources)); }
这样处理后,返回的响应会包含完整的分页信息和HATEOAS链接,同时避免了PersistentEntity为空的异常。
方式二:扩展Spring Data REST的实体元数据(进阶)
如果你希望Spring Data REST自动处理Revision类型,可以注册一个自定义的PersistentEntity实现,或者使用RepresentationModelProcessor来扩展序列化逻辑,但这种方式相对复杂,适合需要全局支持Revision序列化的场景。
内容的提问来源于stack exchange,提问作者drenda

