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 实现步骤
- 定义 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
}
- 创建控制器:
@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 获取。