RESTful 设计与内容协商(Accept/Content-Type)
REST 不是一堆注解,而是「用 HTTP 本来有的语义表达资源操作」的设计风格;内容协商则是让同一个接口按客户端诉求返回不同格式的机制。
RESTful 设计三要素
- 资源用名词复数 URI:/api/users、/api/users/{id}/orders。
- 操作交给 HTTP 方法:GET 查、POST 增、PUT 整体改、PATCH 部分改、DELETE 删。
- 状态用标准状态码表达,错误用统一错误体。
| 场景 | 方法与 URI | 期望状态码 |
|---|---|---|
| 查询列表 | GET /api/users | 200 |
| 按 id 查询 | GET /api/users/1 | 200 / 404 |
| 新建 | POST /api/users | 201 + Location |
| 整体更新 | PUT /api/users/1 | 200 / 204 |
| 部分更新 | PATCH /api/users/1 | 200 |
| 删除 | DELETE /api/users/1 | 204 / 404 |
| 参数非法 | 任意 | 400 |
| 资源冲突 | 任意 | 409 |
示例站约定:https://example.com/api/users/{id}。
方法级映射注解
Spring 5.3+ 提供简写注解,本质是 @RequestMapping(method=...):
@RestController
@RequestMapping("/api/users")
public class UserApi {
@GetMapping("/{id}") // = @RequestMapping(path="/{id}", method=GET)
public User get(@PathVariable Long id) { ... }
@PostMapping // = method=POST
@ResponseStatus(HttpStatus.CREATED)
public User create(@RequestBody UserDTO dto) { ... }
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) { ... }
}
produces / consumes:声明能产/能收的媒体类型
内容协商的第一层约束就在映射注解上:
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public User getAsJson(@PathVariable Long id) { ... }
// produces:只有当 Accept 接受 application/json 时才匹配;都不满足 → 406
// consumes:请求 Content-Type 不匹配 → 415(如要求 application/json 却发来 text/plain)
内容协商:同一个资源多种格式
内容协商(Content Negotiation)决定响应用哪种媒体类型序列化,核心依据是请求头 Accept:
GET /api/users/1
Accept: application/json → JSON(Jackson 序列化)
Accept: application/xml → XML(需引入 jackson-dataformat-xml 等支持)
默认情况下 Spring MVC 主要依据 Accept 头协商;如需支持参数(?format=xml)或路径扩展(.xml)等旧式策略,用 ContentNegotiationConfigurer 显式开启:
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON); // 无 Accept 时的兜底
configurer.mediaType("xml", MediaType.APPLICATION_XML);
// 默认策略与可开启项随版本演进,完整清单以官方文档为准
}
两种请求方向要分清
| 方向 | 关键头 | 出问题时的表现 |
|---|---|---|
| 请求体类型 | Content-Type(客户端声明发了什么) | 不匹配 → 415 |
| 期望返回类型 | Accept(客户端想要什么) | 不满足 → 406 |
| 响应的实际类型 | Content-Type(服务端返回了什么) | 客户端解析失败 |
一个完整的 REST 示例
@RestController
@RequestMapping("/api/users")
public class UserApi {
@GetMapping
public List<User> list() {
return userService.findAll(); // 默认返回 JSON 数组
}
@GetMapping("/{id}")
public ResponseEntity<User> detail(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build()); // 404 用标准状态码表达
}
}
RESTful 让接口「可预测」:URL 是资源、方法是动作、状态码是结果。内容协商让一个资源服务多种客户端(浏览器、App、第三方),配好 produces/consumes 与 Accept,接口的「格式边界」就清晰了。