RESTful 设计与内容协商(Accept/Content-Type)

REST 不是一堆注解,而是「用 HTTP 本来有的语义表达资源操作」的设计风格;内容协商则是让同一个接口按客户端诉求返回不同格式的机制。

RESTful 设计三要素

  1. 资源用名词复数 URI:/api/users、/api/users/{id}/orders。
  2. 操作交给 HTTP 方法:GET 查、POST 增、PUT 整体改、PATCH 部分改、DELETE 删。
  3. 状态用标准状态码表达,错误用统一错误体。
场景方法与 URI期望状态码
查询列表GET /api/users200
按 id 查询GET /api/users/1200 / 404
新建POST /api/users201 + Location
整体更新PUT /api/users/1200 / 204
部分更新PATCH /api/users/1200
删除DELETE /api/users/1204 / 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,接口的「格式边界」就清晰了。

笔记加载中…