3. RESTful API 开发学习指南

3 阅读7分钟

一、RESTful API 概述

1. 什么是 REST?

REST(Representational State Transfer):表述性状态转移,是一种软件架构风格,用于设计网络应用的 API。


2. REST 的核心原则

原则说明
无状态每个请求包含所有信息
统一接口使用标准 HTTP 方法
资源导向一切皆是资源
可缓存响应可被缓存
分层系统客户端不知道是连接到服务器还是代理

3. RESTful API 的特点

特点说明
使用 HTTP 动词GET、POST、PUT、DELETE
使用 URI 标识资源/users/{id}
使用标准状态码200、201、404、500
使用 JSON 格式请求和响应数据

二、HTTP 方法与资源操作

1. HTTP 方法对应 CRUD

HTTP 方法CRUD 操作说明是否幂等
GETRead(查询)获取资源✅ 幂等
POSTCreate(创建)创建新资源❌ 不幂等
PUTUpdate(更新)更新整个资源✅ 幂等
PATCHUpdate(部分更新)更新部分资源❌ 不幂等
DELETEDelete(删除)删除资源✅ 幂等

2. HTTP 方法详解

GET(查询)

用途:获取资源

示例:

GET /api/users           # 获取所有用户
GET /api/users/1         # 获取 ID 为 1 的用户
GET /api/users?name=张   # 查询姓名包含"张"的用户

特点:

  • 幂等:多次请求结果相同
  • 可缓存:响应可被缓存
  • 安全:不修改服务器数据

POST(创建)

用途:创建新资源

示例:

POST /api/users
Content-Type: application/json

{
  "name": "张三",
  "email": "zhangsan@example.com",
  "password": "123456"
}

响应:

HTTP/1.1 201 Created
Location: /api/users/1

{
  "id": 1,
  "name": "张三",
  "email": "zhangsan@example.com"
}

特点:

  • 不幂等:每次请求创建新资源
  • 需要返回 201 状态码
  • 返回新资源的 URI

PUT(更新)

用途:更新整个资源

示例:

PUT /api/users/1
Content-Type: application/json

{
  "id": 1,
  "name": "张三(已更新)",
  "email": "zhangsan@example.com",
  "phone": "13800138000"
}

特点:

  • 幂等:多次请求结果相同
  • 更新整个资源(所有字段)
  • 资源不存在时可以创建(可选)

PATCH(部分更新)

用途:更新资源的部分字段

示例:

PATCH /api/users/1
Content-Type: application/json

{
  "phone": "13900139000"
}

特点:

  • 不幂等:每次请求可能产生不同结果
  • 只更新提供的字段
  • 更适合局部更新

DELETE(删除)

用途:删除资源

示例:

DELETE /api/users/1

响应:

HTTP/1.1 204 No Content

特点:

  • 幂等:多次删除结果相同
  • 返回 204 状态码
  • 响应体为空

三、HTTP 状态码

1. 2xx 成功

状态码说明使用场景
200 OK请求成功GET、PUT、PATCH、DELETE 成功
201 Created资源创建成功POST 创建资源成功
204 No Content请求成功,无内容返回DELETE 删除成功

2. 3xx 重定向

状态码说明使用场景
301 Moved Permanently永久重定向资源永久移动到新位置
302 Found临时重定向资源临时移动到新位置

3. 4xx 客户端错误

状态码说明使用场景
400 Bad Request请求参数错误数据验证失败
401 Unauthorized未授权未登录或 Token 无效
403 Forbidden禁止访问权限不足
404 Not Found资源不存在查询的资源不存在
409 Conflict资源冲突邮箱已被使用
422 Unprocessable Entity无法处理的实体业务规则验证失败

4. 5xx 服务器错误

状态码说明使用场景
500 Internal Server Error服务器内部错误服务器异常
503 Service Unavailable服务不可用服务器过载或维护

四、URI 设计规范

1. URI 设计原则

原则说明
使用名词URI 应该使用名词,而不是动词
使用复数资源名称使用复数形式
层次结构使用 / 表示层级关系
使用小写URI 使用小写字母
使用连字符多个单词使用连字符连接

2. URI 设计示例

不推荐推荐说明
/getUser/users使用名词,不用动词
/user/users使用复数
/Users/users使用小写
/getUserById/users/{id}资源导向
/getUserOrders/users/{id}/orders层级结构

3. 复杂查询

分页查询:

GET /api/users?page=1&size=10

排序查询:

GET /api/users?sort=id,desc

过滤查询:

GET /api/users?name=张&status=active

组合查询:

GET /api/users?page=1&size=10&sort=id,desc&name=张&status=active

五、Spring Boot 实现 RESTful API

1. 创建 Controller

UserController.java:

package com.example.myapp.controller;

import com.example.myapp.dto.CreateUserDTO;
import com.example.myapp.dto.UpdateUserDTO;
import com.example.myapp.dto.UserDTO;
import com.example.myapp.model.User;
import com.example.myapp.service.UserService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    // GET /api/users - 获取所有用户(分页)
    @GetMapping
    public ResponseEntity<Page<UserDTO>> findAll(Pageable pageable) {
        Page<UserDTO> users = userService.findAll(pageable);
        return ResponseEntity.ok(users);
    }
    
    // GET /api/users/{id} - 获取单个用户
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> findById(@PathVariable Long id) {
        UserDTO user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    // POST /api/users - 创建用户
    @PostMapping
    public ResponseEntity<UserDTO> create(@Valid @RequestBody CreateUserDTO userDTO) {
        UserDTO createdUser = userService.create(userDTO);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
    }
    
    // PUT /api/users/{id} - 更新用户
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> update(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserDTO userDTO
    ) {
        UserDTO updatedUser = userService.update(id, userDTO);
        return ResponseEntity.ok(updatedUser);
    }
    
    // PATCH /api/users/{id} - 部分更新用户
    @PatchMapping("/{id}")
    public ResponseEntity<UserDTO> partialUpdate(
            @PathVariable Long id,
            @RequestBody UpdateUserDTO userDTO
    ) {
        UserDTO updatedUser = userService.partialUpdate(id, userDTO);
        return ResponseEntity.ok(updatedUser);
    }
    
    // DELETE /api/users/{id} - 删除用户
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
    
    // GET /api/users/search?name=xxx - 搜索用户
    @GetMapping("/search")
    public ResponseEntity<Page<UserDTO>> search(
            @RequestParam String name,
            Pageable pageable
    ) {
        Page<UserDTO> users = userService.searchByName(name, pageable);
        return ResponseEntity.ok(users);
    }
}

2. DTO 设计

UserDTO(响应 DTO)

package com.example.myapp.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserDTO {
    private Long id;
    private String name;
    private String email;
    private String phone;
    
    // 从 Entity 转换为 DTO
    public static UserDTO fromEntity(User user) {
        return new UserDTO(
                user.getId(),
                user.getName(),
                user.getEmail(),
                user.getPhone()
        );
    }
}

CreateUserDTO(创建用户 DTO)

package com.example.myapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class CreateUserDTO {
    
    @NotBlank(message = "姓名不能为空")
    @Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
    private String name;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 100, message = "密码长度必须在 6-100 个字符之间")
    private String password;
    
    @Size(max = 20, message = "电话号码长度不能超过 20 个字符")
    private String phone;
}

UpdateUserDTO(更新用户 DTO)

package com.example.myapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class UpdateUserDTO {
    
    @Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
    private String name;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Size(max = 20, message = "电话号码长度不能超过 20 个字符")
    private String phone;
}

六、分页与排序

1. Spring Data 分页

Controller

@GetMapping
public ResponseEntity<Page<UserDTO>> findAll(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "id") String sortBy,
        @RequestParam(defaultValue = "asc") String sortDir
) {
    Sort sort = sortDir.equalsIgnoreCase("desc") 
            ? Sort.by(sortBy).descending() 
            : Sort.by(sortBy).ascending();
    Pageable pageable = PageRequest.of(page, size, sort);
    Page<UserDTO> users = userService.findAll(pageable);
    return ResponseEntity.ok(users);
}

响应示例

{
  "content": [
    {
      "id": 1,
      "name": "张三",
      "email": "zhangsan@example.com",
      "phone": "13800138000"
    },
    {
      "id": 2,
      "name": "李四",
      "email": "lisi@example.com",
      "phone": "13900139000"
    }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 10,
    "sort": {
      "empty": false,
      "sorted": true,
      "unsorted": false
    }
  },
  "totalPages": 1,
  "totalElements": 2,
  "last": true,
  "numberOfElements": 2,
  "first": true,
  "empty": false
}

2. 前端分页示例

React 示例

import { useState, useEffect } from 'react';
import axios from 'axios';

function UserList() {
    const [users, setUsers] = useState([]);
    const [page, setPage] = useState(0);
    const [size, setSize] = useState(10);
    const [totalPages, setTotalPages] = useState(0);
    
    const fetchUsers = async () => {
        const response = await axios.get('/api/users', {
            params: { page, size, sortBy: 'id', sortDir: 'desc' }
        });
        setUsers(response.data.content);
        setTotalPages(response.data.totalPages);
    };
    
    useEffect(() => {
        fetchUsers();
    }, [page]);
    
    return (
        <div>
            <table>
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>姓名</th>
                        <th>邮箱</th>
                        <th>电话</th>
                    </tr>
                </thead>
                <tbody>
                    {users.map(user => (
                        <tr key={user.id}>
                            <td>{user.id}</td>
                            <td>{user.name}</td>
                            <td>{user.email}</td>
                            <td>{user.phone}</td>
                        </tr>
                    ))}
                </tbody>
            </table>
            
            <div>
                <button 
                    disabled={page === 0} 
                    onClick={() => setPage(page - 1)}
                >
                    上一页
                </button>
                <span>第 {page + 1} / {totalPages} 页</span>
                <button 
                    disabled={page >= totalPages - 1} 
                    onClick={() => setPage(page + 1)}
                >
                    下一页
                </button>
            </div>
        </div>
    );
}

七、API 版本控制

1. URI 版本控制

@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
    // V1 版本
}

@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {
    // V2 版本
}

示例:

GET /api/v1/users
GET /api/v2/users

2. 请求头版本控制

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping
    @RequestMapping(headers = "X-API-Version=1")
    public ResponseEntity<List<UserDTO>> findAllV1() {
        // V1 版本
    }
    
    @GetMapping
    @RequestMapping(headers = "X-API-Version=2")
    public ResponseEntity<Page<UserDTO>> findAllV2(Pageable pageable) {
        // V2 版本(支持分页)
    }
}

请求:

GET /api/users
X-API-Version: 2

3. 查询参数版本控制

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping
    public ResponseEntity<?> findAll(
            @RequestParam(required = false, defaultValue = "1") String version,
            Pageable pageable
    ) {
        if ("1".equals(version)) {
            // V1 版本
        } else if ("2".equals(version)) {
            // V2 版本
        }
    }
}

请求:

GET /api/users?version=2

八、最佳实践

1. 统一响应格式

统一响应类

ApiResponse.java:

package com.example.myapp.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }
    
    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}

使用统一响应

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<UserDTO>> findById(@PathVariable Long id) {
        UserDTO user = userService.findById(id);
        return ResponseEntity.ok(ApiResponse.success(user));
    }
}

响应:

{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "name": "张三",
    "email": "zhangsan@example.com"
  }
}

2. API 文档

Swagger 配置

添加依赖:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>

访问 Swagger UI:

http://localhost:8080/swagger-ui.html

3. 安全性

HTTPS

生产环境必须使用 HTTPS。

认证与授权

使用 JWT Token 认证,确保 API 安全。

限流

防止恶意攻击,限制 API 调用频率。


九、总结

概念说明
REST表述性状态转移
RESTful API遵循 REST 风格的 API
HTTP 方法GET、POST、PUT、PATCH、DELETE
HTTP 状态码2xx、3xx、4xx、5xx
URI 设计使用名词、复数、小写
分页排序Pageable 接口
版本控制URI、请求头、查询参数