3-1 RESTful API 设计

3 阅读3分钟

3-1 RESTful API 设计

概念解析

REST 六大原则

原则说明
客户端-服务器分离客户端与服务端独立演化
无状态每个请求包含所有必要信息
可缓存响应可标记为可缓存
分层系统允许中间层
统一接口资源通过 URI 标识
按需代码可返回可执行代码(可选)

RESTful 动词

方法语义幂等安全
GET查询资源
POST创建资源
PUT更新资源(完整)
PATCH更新资源(部分)
DELETE删除资源

HTTP 状态码

状态码含义使用场景
200OK成功响应
201Created资源创建成功
204No Content删除成功,无返回
400Bad Request请求参数错误
401Unauthorized未认证
403Forbidden无权限
404Not Found资源不存在
409Conflict资源冲突
500Server Error服务器内部错误

代码示例

1. RESTful Controller

@RestController
@RequestMapping("/api/users")
@Api(tags = "用户管理")
public class UserController {

    @Autowired
    private UserService userService;

    // GET /api/users
    @GetMapping
    public Result<List<User>> list(
            @RequestParam(required = false) String name,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {

        Page<User> result = userService.query(name, page, size);
        return Result.success(result.getContent(), result.getTotalElements());
    }

    // GET /api/users/{id}
    @GetMapping("/{id}")
    public Result<User> get(@PathVariable Long id) {
        return Result.success(userService.getById(id));
    }

    // POST /api/users
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Result<User> create(@Valid @RequestBody UserDTO dto) {
        User user = userService.create(dto);
        return Result.success(user);
    }

    // PUT /api/users/{id}
    @PutMapping("/{id}")
    public Result<User> update(
            @PathVariable Long id,
            @Valid @RequestBody UserDTO dto) {
        dto.setId(id);
        return Result.success(userService.update(dto));
    }

    // DELETE /api/users/{id}
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public Result<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return Result.success(null);
    }

    // PATCH /api/users/{id}/status
    @PatchMapping("/{id}/status")
    public Result<Void> updateStatus(
            @PathVariable Long id,
            @RequestParam Integer status) {
        userService.updateStatus(id, status);
        return Result.success(null);
    }
}

2. 统一响应格式

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {

    private int code;
    private String message;
    private T data;
    private long timestamp;

    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data, System.currentTimeMillis());
    }

    public static <T> Result<T> success(T data, long total) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        Map<String, Object> pageData = new HashMap<>();
        pageData.put("list", data);
        pageData.put("total", total);
        result.setData((T) pageData);
        result.setTimestamp(System.currentTimeMillis());
        return result;
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null, System.currentTimeMillis());
    }

    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null, System.currentTimeMillis());
    }
}

3. API 版本管理

// 方式一:URL 路径版本
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { }

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
    // v2 可以增加新字段,改变响应结构
}

// 方式二:Header 版本
@GetMapping(value = "/users", headers = "X-API-VERSION=1")
public Result<User> getV1(@PathVariable Long id) { }

@GetMapping(value = "/users", headers = "X-API-VERSION=2")
public Result<UserDTO> getV2(@PathVariable Long id) { }

// 方式三:参数版本
@GetMapping(value = "/users", params = "version=1")
public Result<User> getV1(@PathVariable Long id) { }

4. 分页查询

// 分页请求 DTO
@Data
public class PageRequest {
    @Min(1)
    private int page = 1;

    @Min(1)
    @Max(100)
    private int size = 10;
}

// 使用 PageHelper(MyBatis 分页)
@GetMapping
public Result<PageInfo<User>> list(PageRequest request) {
    PageHelper.startPage(request.getPage(), request.getSize());
    List<User> users = userService.list();
    PageInfo<User> pageInfo = new PageInfo<>(users);
    return Result.success(pageInfo);
}

// 使用 JPA Specification
@GetMapping
public Result<Page<User>> list(UserQuery query) {
    Specification<User> spec = (root, cq, cb) -> {
        if (query.getName() != null) {
            cq.where(cb.like(root.get("name"), "%" + query.getName() + "%"));
        }
        cq.orderBy(cb.desc(root.get("createTime")));
        return cq;
    };
    return Result.success(userRepository.findAll(spec, PageRequest.of(query.getPage() - 1, query.getSize())));
}

常见坑点

⚠️ 坑 1:POST 和 PUT 混淆

场景方法说明
创建资源POST服务端生成 ID
更新资源(完整)PUT客户端提供完整资源
更新资源(部分)PATCH只更新部分字段

⚠️ 坑 2:状态码使用错误

// ❌ 创建成功后返回 200
@PostMapping
public Result<User> create(@RequestBody UserDTO dto) {
    return Result.success(userService.create(dto));  // 应返回 201
}

// ✅
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Result<User> create(@RequestBody UserDTO dto) {
    return Result.success(userService.create(dto));
}

⚠️ 坑 3:RESTful 过度设计

// ❌ 过度 RESTful
GET /api/users/{id}/orders/{orderId}/items/{itemId}

// ✅ 资源嵌套不要太深(建议不超过 2 层)
GET /api/items/{itemId}  // 通过 itemId 直接获取

面试题

Q1:GET 和 POST 的区别?

参考答案

维度GETPOST
参数位置URL query stringRequest body
长度限制浏览器限制(约 2KB)无限制
缓存可缓存不可缓存
安全性参数暴露在 URL参数在 body 中
幂等性幂等非幂等
编码URL 编码多种编码

Q2:PUT 和 PATCH 的区别?

参考答案

  • PUT:完整更新,幂等操作

    PUT /users/1
    {
      "name": "张三",
      "age": 25,
      "email": "zhangsan@example.com"  // 必须全部字段
    }
    
  • PATCH:部分更新,非幂等

    PATCH /users/1
    {
      "age": 26  // 只更新 age
    }
    

Q3:如何保证 RESTful API 的安全性?

参考答案

  1. 认证授权:JWT / OAuth2
  2. 参数校验:@Valid + BindingResult
  3. 防 XSS:输入过滤
  4. 防 SQL 注入:使用参数化查询
  5. 限流:接口限流防止滥用
  6. HTTPS:全程加密
  7. 敏感数据脱敏:日志中隐藏敏感信息