You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无需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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 12:43:13