首页 / Spring Boot 入门教程 / RESTful API 设计

Spring Boot 入门教程

RESTful API 设计

本教程共 48 篇 · 第 19 篇 · 更新于 2026-08-13 · 约 3 分钟阅读

Spring BootRESTfulAPI 设计HTTP 状态码RestControllerJSON

本节目标:理解 REST 的核心思想,学会用 HTTP 方法和状态码设计资源接口,写出规范的增删改查 API。

REST 到底在说什么

REST 全称 Representational State Transfer,表现层状态转移。名字拗口,思想很朴素。

它把一切业务对象都看成资源。用户是资源,订单是资源,文章是资源。

每个资源有唯一的 URL 地址。对资源的操作,复用 HTTP 自带的方法,不自己发明动词。

核心约定四条:

  1. 资源用名词命名/users/orders,不用 /getUser
  2. 方法表达动作:GET 查、POST 增、PUT 改、DELETE 删。
  3. 无状态:每个请求独立,服务端不记客户端状态。
  4. 用状态码说话:成功、失败、找不到,都体现在 HTTP 状态码里。

REST 是风格,不是标准。团队内部约定一致,比纠结「是否纯正」更重要。

HTTP 方法与状态码对照

每个方法有它惯用的语义和状态码:

方法语义常用状态码
GET查询资源200 OK
POST新建资源201 Created
PUT整体更新资源200 OK
DELETE删除资源204 No Content

状态码用对地方,调用方只看响应就能判断结果,不用解析业务字段。

Note

POST 和 PUT 都带请求体,区别在语义:POST 是「新增,ID 由服务端定」,PUT 是「替换,ID 由客户端指定」。

URL 怎么设计

好的 URL 像一条路径,从集合走到单个资源:

GET    /products          查询商品列表
POST   /products          新建商品
GET    /products/{id}     查询单个商品
PUT    /products/{id}     更新商品
DELETE /products/{id}     删除商品

几点约定俗成:

  • 用复数名词,/products 而不是 /product
  • 层级关系用斜杠表达,/users/1/orders 表示用户 1 的订单。
  • 过滤条件放查询参数,/products?category=book
  • 不用动词,/products/delete/1 是反面教材。

用 @RestController 写一套接口

下面用 ConcurrentHashMap 当内存仓库,写一个完整的产品接口。不连数据库,专注看 Web 层的写法。

产品模型:

package com.example.demo.model;

public class Product {

    private Long id;
    private String name;
    private Double price;

    public Product() {
    }

    public Product(Long id, String name, Double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    // getter / setter 省略,IDE 自动生成
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
}

控制器:

package com.example.demo.controller;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.model.Product;

@RestController
@RequestMapping("/products")
public class ProductController {

    private final Map<Long, Product> store = new ConcurrentHashMap<>();
    private final AtomicLong idGen = new AtomicLong(1);

    // GET /products —— 查询列表
    @GetMapping
    public List<Product> list() {
        return List.copyOf(store.values());
    }

    // POST /products —— 新建,返回 201
    @PostMapping
    public ResponseEntity<Product> create(@RequestBody Product product) {
        Product saved = new Product(idGen.getAndIncrement(),
                product.getName(), product.getPrice());
        store.put(saved.getId(), saved);
        return ResponseEntity.status(HttpStatus.CREATED).body(saved);
    }

    // GET /products/{id} —— 查询单个
    @GetMapping("/{id}")
    public Product get(@PathVariable Long id) {
        return store.get(id);
    }

    // PUT /products/{id} —— 整体更新
    @PutMapping("/{id}")
    public Product update(@PathVariable Long id, @RequestBody Product product) {
        product.setId(id);
        store.put(id, product);
        return product;
    }

    // DELETE /products/{id} —— 删除,返回 204
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        store.remove(id);
        return ResponseEntity.noContent().build();
    }
}

几个细节值得说:

  • 类上 @RequestMapping("/products") 定前缀,方法上只写剩余路径。
  • @PathVariable 把 URL 里的 {id} 绑定到方法参数。
  • ResponseEntity 能精细控制状态码。不想控制时,直接返回对象也行,默认 200。
  • store.get(id) 查不到时返回 null,接口照样回 200。生产代码应抛异常走 404(第 21 章 ProblemDetail 示例已覆盖),这里仅为演示简化。
  • 删除返回 204 无内容,符合 REST 语义。

用 curl 验证接口

启动项目后,终端里就能测:

# 新建商品
curl -X POST http://localhost:8080/products \
  -H "Content-Type: application/json" \
  -d '{"name":"键盘","price":199.0}'

# 查询列表
curl http://localhost:8080/products

# 更新商品
curl -X PUT http://localhost:8080/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"机械键盘","price":299.0}'

# 删除商品
curl -X DELETE http://localhost:8080/products/1
Tip

POST 后服务端返回 201 和新建的资源。规范的做法是响应头里带 Location 指向新资源地址,调用方拿它就能查。

本节小结

  • REST 把业务抽象成资源,URL 只放名词。
  • GET/POST/PUT/DELETE 对应查、增、改、删。
  • 状态码要选对:201 新建、204 删除、200 成功。
  • @RestController 加 @RequestMapping 就能组织出一套规范接口。