RESTful API 及其 SpringMVC 实现

引言

在现代 Web 开发中,RESTful API 已成为应用程序之间通信的标准方式。

REST(Representational State Transfer,表述性状态转移)是一种软件架构风格。REST 定义了一组约束条件和原则,用于创建可扩展、松耦合的 Web 服务。

REST 不是必须遵守的标准,只是一种设计风格。

是否采用 REST 取决于业务场景,但遵循 REST 风格通常能带来更好的可扩展性、可维护性和可读性。

概念解释

Representational (表述)

REST 中的 R(Representational) 指的是"资源的表述"。

资源本身是抽象概念(例如数据库里的用户记录),客户端从来不会直接摸到资源,而是拿到它的 表述------也就是一个 JSON、XML 或 HTML 文档。

比如 GET /users/123 返回:

json 复制代码
{
  "id": 123,
  "name": "张三",
  "email": "zhangsan@example.com"
}

这个 JSON 就是用户 123 当前状态的一份"表述"。

同样,创建用户时,客户端向 POST /users 发送一份 JSON 表述,服务端根据这份表述去创建一个新资源。

"状态转移"又是什么意思?

客户端通过服务端返回的资源表述,以及表述中可能包含的超链接,来知道接下来可以做什么,从而驱动应用状态发生变化。

举个例子:

客户端调用 GET /users/123 拿到用户信息,这段信息里可能包含一个 orders 链接字段:

json 复制代码
{
  "id": 123,
  "name": "张三",
  "links": {
    "orders": "/users/123/orders"
  }
}

客户端看到这个链接,就知道访问 /users/123/orders 可以查看该用户的订单。

用户从"浏览用户详情"转移到"浏览用户订单"这个新的界面状态,就靠返回数据里的链接来驱动 ------ 这就是 "表述性状态转移" 的字面由来:状态转移由资源的表述来引导。

(实际项目中这个"超链接"部分用得不多,但概念如此;简单的 CRUD 接口即使不包含链接,只要遵守了 URL 名词化 + 正确 HTTP 方法,大家也习惯叫它 RESTful。)

RESTful API

RESTful API 是遵循 REST 架构风格的 Web API 设计风格。它基于 HTTP 协议,通过 URL 定位资源,用 HTTP 方法(GET、POST 等)描述操作,实现客户端与服务器之间的交互。

一句话就是:

URL 中只表示资源,操作由 HTTP 动词来表达。

示例:RESTful 风格的用户接口:

java 复制代码
GET     /users        # 获取用户列表
GET     /users/1      # 获取 id=1 的用户
POST    /users        # 创建用户
PUT     /users/1      # 更新 id=1 的用户
DELETE  /users/1      # 删除 id=1 的用户

RESTful API 的特点

  • 无状态:每个请求包含处理所需的所有信息
  • 统一接口:使用标准 HTTP 方法进行操作
  • 资源导向:所有内容都被抽象为资源

资源和 URI

在 REST 中,所有事物都被抽象为资源,URI 的设计遵循一定的规范:

  • 使用名词而非动词表示资源
  • 使用复数形式命名集合,用路径表示资源层级关系
  • 避免文件扩展名,用 Accept 请求头指定返回格式
  • 查询参数用于过滤、分页、排序

示例:

java 复制代码
GET /users                # 获取用户集合
GET /users/123            # 获取ID为123的用户,配合 Accept: application/json
GET /users/123/orders     # 用户123的订单集合
GET /users/123/orders/20  # 用户123的20号订单集合
GET /users?status=active&page=1&size=10&sort=createTime,desc    

HTTP 方法

方法 用途 幂等性 请求体
GET 获取资源
POST 创建资源
PUT 整体更新
PATCH 局部更新 视实现而定
DELETE 删除资源 通常无

说明:

  • GET 用于查询资源,不应修改服务器状态
  • POST 常用于创建资源,通常不是幂等的
  • PUT 通常用于对资源进行整体替换,因此一般要求提交完整资源表示
  • PATCH 用于部分更新,只提交需要修改的字段。它常常被设计为幂等,但并不天然保证幂等
  • DELETE 用于删除资源,多次执行删除同一资源,对最终结果的影响通常是一致的,因此一般认为是幂等的

HTTP 状态码

响应的状态码不能都返回 200,应该有清晰的状态码表示响应的状态。

状态码 含义 说明 使用场景
200 OK 请求成功 GET 请求成功返回数据
201 Created 资源创建成功 POST 创建资源成功
204 No Content 删除成功 DELETE 成功、更新成功但不返回数据
400 Bad Request 请求有误 请求参数错误
401 Unauthorized 未认证 未登录或 token 过期
403 Forbidden 无权限访问 已登录但无权限
404 Not Found 资源不存在 资源找不到
500 Internal Server Error 服务器内部错误 代码抛异常了

SpringMVC 实现 RESTful API

SpringMVC 提供了丰富的注解,方便实现 RESTful 接口。以用户相关接口为例:

CRUD

java 复制代码
@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping
    public List<User> list() {
        return userService.list();
    }

    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        return userService.getById(id);
    }

    @PostMapping
    public User create(@RequestBody @Valid UserCreateRequest request) {
        return userService.create(request);
    }

    @PutMapping("/{id}")
    public User update(@PathVariable Long id,
                       @RequestBody @Valid UserUpdateRequest request) {
        return userService.update(id, request);
    }

    @PatchMapping("/{id}")
    public User patch(@PathVariable Long id,
                      @RequestBody Map<String, Object> fields) {
        return userService.patch(id, fields);
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

登录注册

登录注册通常不属于标准 CRUD,更适合单独设计认证接口:

java 复制代码
@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/register")
    public Result register(@RequestBody @Valid RegisterRequest request) {
        return authService.register(request);
    }

    @PostMapping("/login")
    public Result login(@RequestBody @Valid LoginRequest request) {
        return authService.login(request);
    }

    @PostMapping("/logout")
    public Result logout() {
        return authService.logout();
    }

    @PostMapping("/refresh")
    public Result refresh(@RequestBody RefreshRequest request) {
        return authService.refreshToken(request);
    }
}

说明:

  • 注册:创建用户资源
  • 登录:生成 token 或 session
  • 刷新 token:更新认证凭证
  • 登出:使 token 失效

严格从 REST 角度看,这类接口不一定完全符合纯粹的资源建模方式,但在实际项目中,这种设计方式更常见,也更易于理解和实现。

统一返回格式

实际项目中会统一返回结构,方便前端处理:

java 复制代码
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
}

示例:

java 复制代码
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "name": "Tom"
  }
}

不过这里需要注意一点:

HTTP 状态码和业务状态码最好分层表达。

也就是说:

  • HTTP 状态码用于表达请求在协议层面是否成功
  • Result.code 用于表达业务层面的状态或错误码

例如:

  • HTTP 返回 404,表示资源不存在
  • 响应体中再返回业务码,如 USER_NOT_FOUND

这样更清晰,也更利于前后端联调和问题排查。

参数校验和异常处理

可以使用 Bean Validation 对请求参数进行校验:

java 复制代码
public class UserCreateRequest {

    @NotBlank(message = "用户名不能为空")
    private String name;

    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄不能小于 0")
    private Integer age;
}

为了统一处理异常,通常还会使用全局异常处理器统一返回错误信息:

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result<String> handleException(Exception e) {
        return Result.fail(500, e.getMessage());
    }
}

如果要进一步完善,项目中一般还会分别处理:

  • 参数校验异常
  • 业务异常
  • 资源不存在异常
  • 权限相关异常

这样返回给前端的信息会更准确

补充

URL 和 URI 的区别

URI(Uniform Resource Identifier)是统一资源标识符,用于标识一个资源。

URL(Uniform Resource Locator)是 URI 的一种,它不仅标识资源,还说明如何定位该资源,也就是告诉你应该通过什么协议、访问哪个主机、走哪条路径去找到它。例如:

text 复制代码
https://example.com/users/1

除了 URL 之外,URI 还包括 URN(Uniform Resource Name):

  • URL:告诉资源"在哪里、怎么访问"
  • URN:只给资源一个唯一名称,不说明资源具体位置

接口的幂等性

幂等性指的是:

同一个请求执行一次和执行多次,对服务器资源状态产生的最终影响是一致的。

例如:

  • 多次执行 GET /users/1,查询结果不会因为请求次数而改变资源状态
  • 多次执行 DELETE /users/1,第一次可能删除成功,后续可能资源已经不存在,但最终资源都是"已删除"状态

需要注意的是,幂等性强调的是对资源状态的影响一致,并不意味着每次响应内容必须完全相同。

总结与思考

RESTful API 的核心思想可以概括为:

  • 用 URL 表示资源
  • 用 HTTP 方法表示操作
  • 用状态码表达请求结果
  • 保持接口设计统一、简洁、清晰

在 SpringMVC 中,可以通过 @RestController@GetMapping@PostMapping@RequestBody@PathVariable 等注解快速实现 RESTful API。

不过需要注意的是,REST 不是一套死板的规范。在实际项目中,接口设计还是要结合业务场景、团队规范以及前后端协作方式灵活处理。

优势

  • 简单直观,易于理解
  • 前后端职责清晰,耦合度较低
  • 基于无状态设计,便于横向扩展
  • 接口风格统一,维护成本较低

局限

  • 对复杂业务动作的建模有时不够直接
  • 对复杂查询场景表达能力有限

常见问题

1、PUT 和 PATCH 有什么区别?实际项目中怎么用的?

回答:PUT 通常用于对资源进行整体更新或替换,PATCH 用于部分更新,只需要传递变更的字段。

在实际项目中,我们使用 PATCH 更多一些,因为大多数场景只是修改某几个字段,没必要每次都传整个对象。不过如果业务上要求前端提交完整资源表示,那么使用 PUT 会更合适。

2、RESTfuI 接口怎么做版本控制?

回答常见的版本控制方式有三种:

  1. URL 路径版本号 :如 /api/v1/users/api/v2/users 优点是简单直观、便于调试,也是最常见的方式。

  2. Header 版本控制 :如 Accept: application/vnd.myapi.v1+json 优点是 URL 更干净,但不够直观,调试也相对麻烦。

  3. 查询参数版本控制 :如 /users?version=1 一般不太推荐,因为查询参数更适合用于过滤、分页、排序等语义。

如果是实际项目落地,大多数团队会优先选择第一种,也就是在 URL 中携带版本号。

3、如果一个操作不是简单的增删改查,比如"审批通过",RESTfuI 怎么设计?

回答:

通常有两种设计思路:

方式一:把操作建模成资源

例如:

http 复制代码
POST /orders/1/approvals

表示为订单 1 创建一条审批记录。

这种方式更符合 REST 的资源导向思想,适用于审批本身就是一个独立业务对象的场景。

方式二:把操作视为状态变更

例如:

http 复制代码
PATCH /orders/1

请求体:

json 复制代码
{
"status": "approved"
}

这种方式更简单直接,适合"审批通过"本质上只是修改订单状态的场景。

实际项目中要看业务复杂度。如果审批过程本身有记录、流转、意见等信息,通常更适合建模成独立资源;如果只是简单改状态,用 PATCH 会更高效。

4、RESTful 接口返回的错误信息应该怎么设计?

首先,HTTP 状态码要使用正确:

  • 400 表示客户端请求有误
  • 401 表示未认证
  • 403 表示无权限
  • 404 表示资源不存在
  • 500 表示服务端内部错误

其次,响应体中建议包含:

  • 业务错误码:便于前端识别错误类型
  • 错误信息:便于展示或排查问题
  • 详细描述:可选,便于开发调试
  • traceId:可选,便于定位日志

例如:

json 复制代码
{
  "code": "USER_NOT_FOUND",
  "message": "用户不存在",
  "description": "userId=123",
  "traceId": "a1b2c3d4"
}
相关推荐
Gopher_HBo1 小时前
阻塞队列之DelayQueue
后端
SamDeepThinking1 小时前
你认为从0-1开发一个项目最难的地方是什么?
java·后端·架构
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第43题】【JVM篇】第3题:GC分为哪两种?Young GC 和 Full GC有什么区别?
java·开发语言·jvm·后端·面试
Bear on Toilet2 小时前
【JSON-RPC远程过程调用组件库】测试报告
开发语言·软件测试·后端·自动化脚本
金玉满堂@bj2 小时前
Go 语言能做什么?
开发语言·后端·golang
ooseabiscuit2 小时前
Laravel6.x新特性全解析
java·开发语言·后端·mysql·spring
李日灐2 小时前
< 9 > Linux 进程:进程状态 + 进程切换 + 附带常用指令(jobs / fg / kill / ps)
linux·运维·服务器·后端·面试·进程状态
枕星而眠2 小时前
一篇吃透 C++ 核心基础:初始化、引用、指针、内联、重载、右值引用
开发语言·数据结构·c++·后端·visual studio
cong_2 小时前
狐蒂云🦊跑路我的摸鱼岛没了!
前端·后端·github