乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

RESTful

本章是 Spring MVC 教程中"请求映射"章节的设计思想基础。在讲解 @GetMapping、@PostMapping 等注解的语法之前,必须先理解 RESTful 设计思想——它回答了"URL 应该怎么设计""HTTP 方法应该怎么用"等根本问题。RESTful 不是 Spring MVC 的专属概念,但 Spring MVC 的注解体系(如 @PathVariable、@RequestBody)是为 RESTful 风格量身打造的。不理解 RESTful,注解只是死记硬背的符号。


定义与作用

RESTful 是一种软件架构风格,全称 REpresentational State Transfer(表述性状态转移),由 Roy Fielding 在 2000 年的博士论文中提出。它的核心思想是:用 URL 定位资源,用 HTTP 方法描述对资源的操作。

传统 Web 开发中,URL 是"动作 + 对象"的混合体,如 /userDelete?id=1、/userView?id=1。RESTful 则要求 URL 只表示资源本身(名词),操作意图通过 HTTP 方法(GET/POST/PUT/DELETE)表达。同一个 URL /users/1,用 GET 访问是"查询",用 DELETE 访问是"删除",用 PUT 访问是"更新"。

RESTful 不是协议、不是标准、不是框架,它是一种设计约束。遵循这些约束的系统,天然具备可读性、可缓存性、可扩展性。Spring MVC 的 @RestController、@PathVariable、@RequestBody 等特性,本质上都是为了方便开发者构建 RESTful 风格的接口。

生活类比:图书馆管理系统

想象你去图书馆借书:

  • 传统 URL 风格:图书馆有 4 个窗口,分别写着"查询图书窗口"、"借阅图书窗口"、"归还图书窗口"、"删除图书记录窗口"。每个窗口的办事流程完全不同,你要记住哪个窗口办什么事。
  • RESTful 风格:图书馆只有一个"图书资源柜台"(/books)。你走到柜台前,用不同的"办事方式"表达意图:
    • GET /books/123 → "请把 123 号图书的信息给我看看"
    • POST /books → "我要新增一本图书,信息在申请表里"
    • PUT /books/123 → "123 号图书的信息变了,请按这张表更新"
    • DELETE /books/123 → "请把 123 号图书记录注销掉"

这个类比的关键在于:URL 只标识资源(图书),动作由 HTTP 方法表达。柜台工作人员(服务器)看到 GET 就知道是查询,看到 DELETE 就知道是删除,无需在 URL 里写动词。


核心原理

RESTful 的六大设计原则

原则含义在 Web 开发中的体现
资源识别系统中的一切都是资源,每个资源有唯一标识URL 如 /users/1、/orders/2024-001
统一接口对资源的操作通过统一的 HTTP 方法表达GET 查询、POST 创建、PUT 全量更新、DELETE 删除
无状态服务器不保存客户端的上下文状态每个请求携带完整信息(如 Token),服务器不依赖之前的请求
可缓存响应可以被客户端或中间层缓存GET 响应可设 Cache-Control,减少重复请求
分层系统客户端不需要知道是否直连服务器负载均衡、CDN、反向代理对客户端透明
按需代码服务器可向客户端下发可执行代码(可选)JavaScript 脚本动态下发(较少使用)

重点理解前三个原则:

  1. 资源识别要求开发者把业务对象抽象为资源。"用户"是资源、"订单"是资源、"文章评论"也是资源。资源可以嵌套:/articles/42/comments/3 表示"42 号文章的 3 号评论"。

  2. 统一接口是 RESTful 最直观的特征。同样的 URL,不同的 HTTP 方法,含义完全不同。这要求前端开发者理解 HTTP 协议语义,不能把所有操作都用 POST 完成。

  3. 无状态意味着服务器不会在内存中保存"当前用户是谁"这类会话信息。每个请求必须自包含认证凭证(如 JWT Token 或 Session ID Cookie)。这让服务器可以水平扩展——请求发到任意一台机器都能正确处理。

RESTful URL 设计规范

规范正确示例错误示例说明
用名词,不用动词GET /usersGET /getUsersURL 标识资源,动作由 HTTP 方法表达
用复数形式GET /users/1GET /user/1集合资源用复数,表示一类资源
层级关系用 / 分隔GET /users/1/ordersGET /userOrders?userId=1体现资源嵌套关系
避免查询参数做主要标识GET /users/1GET /users?id=1资源 ID 应放在路径中
查询参数仅用于过滤/分页GET /users?page=2&size=10—? 后的参数是查询条件,不是资源标识
统一使用小写/user-profiles/UserProfiles避免大小写敏感问题
用连字符 - 分隔单词/user-profiles/user_profiles下划线在 URL 中可能被浏览器隐藏

HTTP 方法与 CRUD 对应关系

HTTP 方法CRUD 操作幂等性用途Spring MVC 注解
GETRead幂等查询资源@GetMapping
POSTCreate非幂等创建资源@PostMapping
PUTUpdate幂等全量更新资源@PutMapping
PATCHUpdate非幂等部分更新资源@PatchMapping
DELETEDelete幂等删除资源@DeleteMapping

幂等性说明:幂等操作执行一次和执行 N 次,结果相同。GET/PUT/DELETE 是幂等的——多次查询不会改数据,多次全量更新为同样内容结果不变,多次删除同一资源最终都是"已删除"。POST 是非幂等的——两次 POST 可能创建两条记录。

RESTful vs 传统 URL 设计对比

操作传统 URL 风格RESTful 风格对比分析
查询用户列表GET /user/listGET /usersRESTful 用复数名词,无需 list 动词
查询单个用户GET /user/view?id=1GET /users/1RESTful 用路径参数标识资源,URL 更简洁
创建用户POST /user/addPOST /usersRESTful 用 POST 表达"创建",URL 无动词
更新用户POST /user/update?id=1PUT /users/1RESTful 用 PUT 表达"更新",语义更精确
删除用户GET /userDelete?id=1DELETE /users/1传统风格用 GET 做删除,违背 HTTP 语义且不安全
查询用户的订单GET /orderList?userId=1GET /users/1/ordersRESTful 用层级路径表达资源关系

RESTful 架构风格图解

图解说明:

  1. 资源层:/users、/orders 等 URL 只标识资源,不表达动作
  2. 方法层:GET/POST/PUT/DELETE 表达要对资源做什么
  3. 状态层:服务器上的资源状态被 HTTP 方法改变,客户端通过响应获取资源的"表述"(Representation)

完整示例

场景

飞翔科技要开发一个员工管理系统 API,供内部 OA 系统和移动端 App 调用。CTO 大翔要求接口设计必须遵循 RESTful 规范,"让 URL 一看就懂,让 HTTP 方法用对地方"。架构师白歌负责制定 URL 设计规范,小崔负责 Controller 实现,黄俪负责前端对接,李眉负责 API 文档和测试。

白歌的 RESTful 设计规范

资源:员工(employees)、部门(departments)

GET    /employees              → 查询员工列表(支持 ?dept=技术部&page=1)
GET    /employees/{id}          → 查询指定员工
POST   /employees               → 创建新员工(请求体含员工信息)
PUT    /employees/{id}          → 全量更新员工信息
PATCH  /employees/{id}          → 部分更新(如只改手机号)
DELETE /employees/{id}          → 删除员工

GET    /departments/{id}/employees → 查询某部门下的所有员工

黄俪的反馈:"以前对接的接口全是 POST,现在看到 GET 就知道是查数据,看到 DELETE 就知道要小心。URL 里没动词,但 HTTP 方法本身就是动词,组合起来语义特别清晰。"

小崔的 Controller 实现

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    // GET /employees?page=1&size=10
    @GetMapping
    public Page<Employee> list(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        return employeeService.findPage(page, size);
    }

    // GET /employees/1001
    @GetMapping("/{id}")
    public Employee getById(@PathVariable Long id) {
        return employeeService.findById(id);
    }

    // POST /employees
    @PostMapping
    public ResponseEntity<Employee> create(@RequestBody @Valid EmployeeDTO dto) {
        Employee employee = employeeService.create(dto);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(employee.getId())
                .toUri();
        return ResponseEntity.created(location).body(employee);
    }

    // PUT /employees/1001
    @PutMapping("/{id}")
    public Employee update(@PathVariable Long id, @RequestBody @Valid EmployeeDTO dto) {
        return employeeService.update(id, dto);
    }

    // DELETE /employees/1001
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        employeeService.delete(id);
    }
}

代码分析:

  • @RestController 表明该类所有方法返回数据(JSON),而非视图名
  • @PathVariable 从 URL 路径中提取资源 ID,如 /employees/1001 中的 1001
  • @RequestBody 从请求体中解析 JSON 数据,映射为 DTO 对象
  • ResponseEntity.created(location) 返回 201 状态码,并在响应头中携带新资源的 URL——这是 RESTful 的规范做法

李眉的 API 测试用例

# 查询员工列表
curl -X GET http://localhost:8080/employees?page=1&size=5

# 查询单个员工
curl -X GET http://localhost:8080/employees/1001

# 创建员工
curl -X POST http://localhost:8080/employees \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","dept":"技术部","phone":"13800138000"}'

# 更新员工
curl -X PUT http://localhost:8080/employees/1001 \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","dept":"架构部","phone":"13800138000"}'

# 删除员工
curl -X DELETE http://localhost:8080/employees/1001

李眉的测试报告:

  • GET 请求无副作用,可重复执行,适合缓存
  • POST 创建后返回 201 + Location 头,前端可直接跳转新资源页
  • DELETE 返回 204 No Content,表示操作成功但无响应体——符合 RESTful 规范

易错场景与面试考点

误区一:把所有操作都用 POST

错误设计:

@RestController
public class UserController {

    @PostMapping("/user/query")    // ❌ 查询用 POST
    public User query(@RequestBody QueryForm form) { }

    @PostMapping("/user/delete")  // ❌ 删除用 POST
    public void delete(@RequestBody DeleteForm form) { }

    @PostMapping("/user/update")  // ❌ 更新用 POST
    public void update(@RequestBody UpdateForm form) { }
}

纠正:这种做法被称为"HTTP 协议滥用"或"伪 RESTful"。虽然功能上能跑,但丧失了 RESTful 的核心优势:

  • 缓存失效:POST 响应默认不可缓存,而 GET 响应可被浏览器、CDN 缓存
  • 语义模糊:运维人员看日志时,无法从 HTTP 方法判断操作类型
  • 工具支持缺失:浏览器直接输入 URL 只能发 GET,反向代理的安全规则通常按 HTTP 方法配置

误区二:URL 里混用动词和名词

错误设计:

GET /users/getUserInfo/1       ❌ 动词 getUserInfo 多余
POST /users/createUser         ❌ 动词 create 多余,POST 已表达创建
POST /users/1/updatePhone      ❌ 动词 update 多余,PUT 已表达更新

纠正:

GET /users/1                   ✅ 查询用户信息
POST /users                    ✅ 创建用户
PUT /users/1                   ✅ 更新用户(或 PATCH /users/1 部分更新)

误区三:在 URL 里放动作状态

错误设计:POST /users/1/enable 表示"启用用户",POST /users/1/disable 表示"禁用用户"。

纠正:"启用/禁用"是资源状态的变更,应该用 PUT/PATCH 更新资源的 status 字段:

PATCH /users/1
{ "status": "ENABLED" }

如果业务上"启用"是一个复杂操作(涉及发送邮件、初始化权限等),可以设计为子资源或动作资源:

POST /users/1/activation   # 将"激活"视为一个独立资源创建

面试高频:RESTful 的幂等性是什么意思?哪些方法是幂等的?

标准回答:

幂等性是指同样的操作执行一次和执行多次,对系统状态的影响相同。

  • GET:幂等。多次查询不会修改数据。
  • PUT:幂等。将用户年龄改为 25,执行 1 次和 100 次结果相同。
  • DELETE:幂等。删除 ID 为 1 的用户,第一次删除后用户不存在;再执行 99 次,用户仍然不存在——系统状态不变。
  • POST:非幂等。两次 POST /orders 会创建两条订单记录。
  • PATCH:通常非幂等。如 PATCH { "age": "+1" } 执行两次会加 2 岁。

实际意义:幂等操作在网络超时后可以安全重试,不会导致重复副作用。例如 DELETE 请求超时后重试,不会误删其他数据。


小结

RESTful 是一种用 URL 标识资源、用 HTTP 方法表达操作的架构风格。它的六大原则(资源识别、统一接口、无状态、可缓存、分层系统、按需代码)指导开发者设计出语义清晰、可缓存、可扩展的 Web API。

Spring MVC 的 @RestController、@PathVariable、@RequestBody、ResponseEntity 等特性,本质上都是为 RESTful 风格服务的。理解 RESTful 后,这些注解不再是孤立的语法点,而是"如何用 Spring MVC 实现 RESTful 设计"的具体工具。

本章与全局的关系:本章回答了"URL 应该怎么设计""HTTP 方法应该怎么用"。下一章"@RequestMapping 与派生注解"将深入讲解 Spring MVC 如何用注解把 RESTful 设计映射为具体代码。

上一页
PathVariable
下一页
请求映射原理