无需Spring Data Rest,Spring实现HATEOAS+分页的简易方案?
简洁实现Spring Boot+JPA下带HATEOAS与分页的REST API
我完全懂你的痛点——之前找的方案要么要写一堆重复的Assembler样板代码,要么关联实体的HATEOAS链接缺失,想要一个低冗余的实现方式对吧?其实Spring HATEOAS本身就有更简洁的玩法,不用依赖Spring Data Rest,我给你分享几个实用的思路:
1. 实体直接继承RepresentationModel+PagedResourcesAssembler处理分页
这种方式能快速给单个实体和分页结果添加链接,关联实体的链接也能轻松补上:
第一步:改造实体类
让你的实体直接继承RepresentationModel<T>,这样实体本身就具备添加链接的能力:
public class Album extends RepresentationModel<Album> { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String description; private LocalDate releaseDate; @OneToMany(mappedBy = "album") private List<Actor> actors; // Getter、Setter省略 } public class Actor extends RepresentationModel<Actor> { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; @ManyToOne private Album album; // Getter、Setter省略 }
第二步:控制器中处理分页与链接
利用Spring提供的PagedResourcesAssembler自动生成分页链接,同时遍历数据给关联实体添加self链接:
@RestController @RequestMapping("/api/albums-list") public class AlbumController { private final AlbumRepository albumRepository; public AlbumController(AlbumRepository albumRepository) { this.albumRepository = albumRepository; } @GetMapping public PagedModel<EntityModel<Album>> getAlbums(Pageable pageable, PagedResourcesAssembler<Album> assembler) { // 查询分页数据 Page<Album> albumPage = albumRepository.findAll(pageable); // 给每个Album和它的Actor添加self链接 albumPage.forEach(album -> { // 给当前Album加self链接 album.add(linkTo(methodOn(AlbumController.class).getAlbum(album.getId())).withSelfRel()); // 给关联的每个Actor加self链接 album.getActors().forEach(actor -> { actor.add(linkTo(methodOn(ActorController.class).getActor(actor.getId())).withSelfRel()); }); }); // 利用assembler自动生成分页的first/prev/next/last链接 return assembler.toModel(albumPage, EntityModel::of); } @GetMapping("/{id}") public EntityModel<Album> getAlbum(@PathVariable Long id) { Album album = albumRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Album not found with id: " + id)); album.add(linkTo(methodOn(AlbumController.class).getAlbum(id)).withSelfRel()); return EntityModel.of(album); } } // 对应的Actor控制器 @RestController @RequestMapping("/api/actors") public class ActorController { private final ActorRepository actorRepository; public ActorController(ActorRepository actorRepository) { this.actorRepository = actorRepository; } @GetMapping("/{id}") public EntityModel<Actor> getActor(@PathVariable Long id) { Actor actor = actorRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Actor not found with id: " + id)); actor.add(linkTo(methodOn(ActorController.class).getActor(id)).withSelfRel()); return EntityModel.of(actor); } }
这样返回的结果就会和你期望的结构一致:包含分页的_links、page信息,以及_embedded里每个Album和Actor的self链接。
2. DTO+ModelMapper分离实体与视图(更优雅的方式)
如果不想让实体类继承RepresentationModel(保持实体纯粹),可以用DTO来承载视图数据并添加链接,结合ModelMapper减少字段映射的样板代码:
第一步:创建DTO类
public class AlbumDto extends RepresentationModel<AlbumDto> { private Long id; private String title; private String description; private String releaseDate; private List<ActorDto> actors; // Getter、Setter省略 } public class ActorDto extends RepresentationModel<ActorDto> { private Long id; private String firstName; private String lastName; // Getter、Setter省略 }
第二步:控制器中完成实体到DTO的转换与链接添加
@RestController @RequestMapping("/api/albums-list") public class AlbumController { private final AlbumRepository albumRepository; private final ModelMapper modelMapper; public AlbumController(AlbumRepository albumRepository, ModelMapper modelMapper) { this.albumRepository = albumRepository; this.modelMapper = modelMapper; } @GetMapping public PagedModel<EntityModel<AlbumDto>> getAlbums(Pageable pageable, PagedResourcesAssembler<Album> assembler) { Page<Album> albumPage = albumRepository.findAll(pageable); // 转换为DTO并添加链接 Page<AlbumDto> albumDtoPage = albumPage.map(album -> { AlbumDto dto = modelMapper.map(album, AlbumDto.class); // 给AlbumDto加self链接 dto.add(linkTo(methodOn(AlbumController.class).getAlbum(album.getId())).withSelfRel()); // 给关联的ActorDto加链接 dto.setActors(album.getActors().stream().map(actor -> { ActorDto actorDto = modelMapper.map(actor, ActorDto.class); actorDto.add(linkTo(methodOn(ActorController.class).getActor(actor.getId())).withSelfRel()); return actorDto; }).collect(Collectors.toList())); return dto; }); // 转换为带分页链接的PagedModel return assembler.toModel(albumPage, album -> EntityModel.of(modelMapper.map(album, AlbumDto.class))); } @GetMapping("/{id}") public EntityModel<AlbumDto> getAlbum(@PathVariable Long id) { Album album = albumRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Album not found with id: " + id)); AlbumDto dto = modelMapper.map(album, AlbumDto.class); dto.add(linkTo(methodOn(AlbumController.class).getAlbum(id)).withSelfRel()); return EntityModel.of(dto); } }
这种方式保持了实体类的纯粹性,同时DTO可以灵活调整返回的字段,适合复杂的视图场景。
关键要点总结
- 分页链接自动生成:
PagedResourcesAssembler会根据Pageable参数自动拼接first/prev/next/last的URL,不用手动拼接,避免出错。 - 关联实体链接:只要遍历关联集合,给每个关联对象添加对应的self链接即可,用
WebMvcLinkBuilder可以基于控制器方法生成链接,不用硬编码URL。 - 减少样板代码:不用写大量的
RepresentationModelAssemblerSupport子类,直接在控制器或转换逻辑中处理链接,代码更简洁。
内容的提问来源于stack exchange,提问作者sylleryum
相关产品推荐
相关产品推荐

