1. 概述

Spring Data Rest 模块能让我们快速搭建 RESTful 服务,但它有个默认行为可能让人困惑:实体 ID 默认不会序列化到响应中。本文将深入探讨这个设计背后的原因,并提供多种解决方案来暴露实体 ID。

提示:Spring Data Rest 的设计哲学是资源标识通过 URL 体现,而非暴露内部 ID。但实际开发中我们常需要访问 ID,这就需要手动配置了。

2. 默认行为

先看个简单例子理解问题。定义一个 Person 实体:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // getters and setters
}

对应的仓库接口:

public interface PersonRepository extends JpaRepository<Person, Long> {
}

添加 Spring Boot 依赖后自动启用功能:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

访问 http://localhost:8080/persons 时,默认响应如下:

{
    "_embedded" : {
        "persons" : [ {
            "name" : "John Doe",
            "_links" : {
                "self" : {
                    "href" : "http://localhost:8080/persons/1"
                },
                "person" : {
                    "href" : "http://localhost:8080/persons/1{?projection}",
                    "templated" : true
                }
            }
        }, ...]
    ...
}

关键发现

  • 响应中只有 name 字段,id 字段被过滤
  • 这是 Spring Data Rest 的刻意设计:内部 ID 对外部系统无意义
  • REST 架构中资源标识由 URL 承担

踩坑提醒:这个行为只影响 Spring Data Rest 的自动接口,自定义的 @RestController 不受影响(除非用了 Spring HATEOAS 的模型类)

3. 使用 RepositoryRestConfigurer

最直接的解决方案:通过 RepositoryRestConfigurer 配置暴露 ID:

@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {

    @Override
    public void configureRepositoryRestConfiguration(
      RepositoryRestConfiguration config, CorsRegistry cors) {
        config.exposeIdsFor(Person.class);
    }
}

版本兼容注意

  • Spring Boot 2.1+(Spring Data Rest 3.1+)使用上述方式
  • 旧版本需用已废弃的 RepositoryRestConfigurerAdapter
@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(Person.class);
    }
}

配置后响应包含 ID:

{
    "_embedded" : {
        "persons" : [ {
            "id" : 1,
            "name" : "John Doe",
            "_links" : { ... }
        }, ...]
    ...
}

优化方案:当实体很多时,用 JPA 元数据自动处理所有实体:

@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void configureRepositoryRestConfiguration(
      RepositoryRestConfiguration config, CorsRegistry cors) {
        Class[] classes = entityManager.getMetamodel()
          .getEntities().stream().map(Type::getJavaType).toArray(Class[]::new);
        config.exposeIdsFor(classes);
    }
}

4. 使用 @Projection

通过投影接口控制字段暴露:

@Projection(name = "person-view", types = Person.class)
public interface PersonView {

    Long getId();

    String getName();
}

访问时需指定投影参数:http://localhost:8080/persons?projection=person-view

{
    "_embedded" : {
        "persons" : [ {
            "id" : 1,
            "name" : "John Doe",
            "_links" : { ... }
        }, ...]
    ...
}

仓库级配置:用 @RepositoryRestResource 设置默认投影:

@RepositoryRestResource(excerptProjection = PersonView.class)
public interface PersonRepository extends JpaRepository<Person, Long> {
}

重要限制:excerptProjection 只对集合资源生效,单资源仍需手动加 ?projection=person-view

字段顺序控制:添加 @JsonPropertyOrder 保证 ID 在前:

@JsonPropertyOrder({"id", "name"})
@Projection(name = "person-view", types = Person.class)
public interface PersonView { 
    //...
}

5. 使用 DTO 覆盖仓库接口

通过自定义控制器完全接管响应:

5.1 实现步骤

  1. 定义 DTO
public class PersonDto {

    private Long id;

    private String name;

    public PersonDto(Person person) {
        this.id = person.getId();
        this.name = person.getName();
    }
    
    // getters and setters
}
  1. 创建控制器
@RepositoryRestController
public class PersonController {

    @Autowired
    private PersonRepository repository;

    @GetMapping("/persons")
    ResponseEntity<?> persons(PagedResourcesAssembler resourcesAssembler) {
        Page<Person> persons = this.repository.findAll(Pageable.ofSize(20));
        Page<PersonDto> personDtos = persons.map(PersonDto::new);
        PagedModel<EntityModel<PersonDto>> pagedModel = resourcesAssembler.toModel(personDtos);
        return ResponseEntity.ok(pagedModel);
    }
}

关键配置要点

  • 必须用 @RepositoryRestController 而非 @RestController
  • 路径需与仓库默认路径一致
  • 类必须能被 Spring 扫描到

响应示例:

{
    "_embedded" : {
        "personDtoes" : [ {
            "id" : 1,
            "name" : "John Doe"
        }, ...]
    }, ...
}

5.2 方案缺点

虽然灵活但需权衡:

  • 代码量激增:每个接口都要手动实现
  • 框架优势丢失:放弃 Spring Data Rest 的自动化能力
  • 维护成本高:需同步维护实体和 DTO

简单粗暴结论:除非有特殊定制需求,优先考虑前两种方案

6. 总结

Spring Data Rest 默认隐藏实体 ID 是合理的设计,但提供了多种暴露方式:

方案 适用场景 优点 缺点
RepositoryRestConfigurer 全局配置 一次配置永久生效 无法细粒度控制
@Projection 按需暴露 灵活控制字段 需修改请求参数
DTO 覆盖 完全定制 最大控制权 开发成本高

推荐实践

  • 优先用 RepositoryRestConfigurer 全局暴露
  • 需要精细控制时结合 @Projection
  • 避免轻易采用 DTO 方案

所有示例代码可在 GitHub 获取。


原始标题:Spring Data Rest – Serializing the Entity ID