HTTP协议与RESTful API设计

学习目标

  • 深入理解HTTP协议的工作原理和核心概念
  • 掌握HTTP请求方法、状态码、请求头和响应头的使用
  • 理解RESTful架构风格和设计原则
  • 能够设计符合RESTful规范的API接口
  • 掌握API版本管理、安全设计和最佳实践
  • 能够在电商项目中设计和实现完整的商品API
  • 理解HTTPS加密原理和安全通信机制
  • 掌握API文档编写和接口测试方法

知识体系思维导图

HTTP与RESTful API

HTTP协议基础

协议概述

请求响应模型

无状态特性

持久连接

请求方法

GET查询

POST创建

PUT更新

DELETE删除

PATCH部分更新

状态码

1xx信息

2xx成功

3xx重定向

4xx客户端错误

5xx服务器错误

请求头响应头

Content-Type

Authorization

Cache-Control

Cookie/Set-Cookie

RESTful设计

核心原则

资源导向

统一接口

无状态

可缓存

URL设计

资源命名

层级关系

查询参数

响应设计

统一格式

错误处理

分页排序

版本管理

URL版本

Header版本

参数版本

API安全

认证授权

JWT Token

OAuth2.0

API Key

数据加密

HTTPS

敏感信息加密

防护措施

限流

防重放

签名验证

最佳实践

接口规范

命名规范

参数规范

响应规范

文档编写

Swagger

接口说明

示例代码

测试调试

Postman

单元测试

集成测试

一、HTTP协议基础

1.1 为什么需要HTTP协议?

在互联网世界中,客户端(浏览器、移动App)和服务器之间需要进行数据交换。但是:

问题场景:

用户在浏览器输入 www.example.com
↓
浏览器如何找到服务器?
浏览器如何告诉服务器要什么数据?
服务器如何返回数据?
数据用什么格式传输?

没有统一协议的问题:

  • 每个应用都要自己定义通信格式
  • 不同系统之间无法互通
  • 开发效率低,维护成本高
  • 安全性难以保证

HTTP协议的解决方案:

HTTP(HyperText Transfer Protocol)超文本传输协议
- 定义了客户端和服务器之间的通信规则
- 规定了请求和响应的格式
- 提供了统一的方法和状态码
- 支持多种数据类型传输

1.2 HTTP协议核心概念

1.2.1 请求-响应模型
服务器 客户端 (浏览器/App) 服务器 客户端 (浏览器/App) 请求行 + 请求头 + 请求体 状态行 + 响应头 + 响应体 HTTP请求 (Request) 处理请求 HTTP响应 (Response)

HTTP请求结构:

GET /api/products/1 HTTP/1.1              ← 请求行(方法 路径 协议版本)
Host: www.example.com                      ← 请求头
User-Agent: Mozilla/5.0
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
                                           ← 空行(分隔请求头和请求体)
                                           ← 请求体(GET请求通常没有请求体)

HTTP响应结构:

HTTP/1.1 200 OK                            ← 状态行(协议版本 状态码 状态描述)
Content-Type: application/json             ← 响应头
Content-Length: 156
Cache-Control: max-age=3600
                                           ← 空行(分隔响应头和响应体)
{                                          ← 响应体
  "id": 1,
  "name": "iPhone 15 Pro",
  "price": 7999.00
}
1.2.2 无状态特性
HTTP协议是无状态的(Stateless)
每次请求都是独立的,服务器不会记住之前的请求

无状态的影响:

// 场景:用户登录后访问个人信息
// 第一次请求:登录
POST /api/login
{
  "username": "zhangsan",
  "password": "123456"
}
// 响应:登录成功

// 第二次请求:获取个人信息
GET /api/user/profile
// 问题:服务器不知道这是刚才登录的用户!

解决方案:

// 方案1:使用Cookie
// 登录成功后,服务器返回Set-Cookie
HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; Path=/; HttpOnly

// 后续请求自动携带Cookie
GET /api/user/profile
Cookie: sessionId=abc123

// 方案2:使用Token(推荐)
// 登录成功后,返回Token
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

// 后续请求在Header中携带Token
GET /api/user/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
1.2.3 持久连接

HTTP/1.0 - 短连接:

客户端 → 建立TCP连接 → 服务器
客户端 → 发送HTTP请求 → 服务器
客户端 ← 接收HTTP响应 ← 服务器
客户端 → 关闭TCP连接 → 服务器

问题:每次请求都要建立和关闭连接,效率低

HTTP/1.1 - 持久连接(Keep-Alive):

客户端 → 建立TCP连接 → 服务器
客户端 → 发送HTTP请求1 → 服务器
客户端 ← 接收HTTP响应1 ← 服务器
客户端 → 发送HTTP请求2 → 服务器(复用连接)
客户端 ← 接收HTTP响应2 ← 服务器
...
客户端 → 关闭TCP连接 → 服务器

优势:减少TCP连接开销,提高性能

持久连接配置:

# 请求头
Connection: keep-alive
Keep-Alive: timeout=5, max=100

# 响应头
Connection: keep-alive
Keep-Alive: timeout=5, max=100

1.3 HTTP请求方法详解

1.3.1 GET - 获取资源

特点:

  • 用于获取资源,不应该修改服务器数据
  • 参数在URL中,可见且有长度限制
  • 可以被缓存
  • 可以被收藏为书签
  • 幂等性:多次请求结果相同

使用场景:

// 1. 获取单个资源
GET /api/products/1

// 2. 获取资源列表
GET /api/products

// 3. 带查询参数
GET /api/products?category=phone&page=1&size=10

// 4. 搜索
GET /api/products/search?keyword=iPhone

Java实现示例:

package com.example.shop.controller;

import com.example.shop.entity.Product;
import com.example.shop.service.ProductService;
import com.example.shop.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 商品控制器
 * 
 * @author 张三
 * @since 1.0.0
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    /**
     * 获取单个商品详情
     * 
     * @param id 商品ID
     * @return 商品信息
     */
    @GetMapping("/{id}")
    public Result<Product> getProduct(@PathVariable Long id) {
        Product product = productService.findById(id);
        if (product == null) {
            return Result.error("商品不存在");
        }
        return Result.success(product);
    }
    
    /**
     * 获取商品列表(支持分页和筛选)
     * 
     * @param category 分类(可选)
     * @param keyword 搜索关键词(可选)
     * @param page 页码(默认1)
     * @param size 每页大小(默认10)
     * @return 商品列表
     */
    @GetMapping
    public Result<List<Product>> getProducts(
            @RequestParam(required = false) String category,
            @RequestParam(required = false) String keyword,
            @RequestParam(defaultValue = "1") Integer page,
            @RequestParam(defaultValue = "10") Integer size) {
        
        List<Product> products = productService.findProducts(
            category, keyword, page, size);
        return Result.success(products);
    }
    
    /**
     * 搜索商品
     * 
     * @param keyword 搜索关键词
     * @return 商品列表
     */
    @GetMapping("/search")
    public Result<List<Product>> searchProducts(
            @RequestParam String keyword) {
        List<Product> products = productService.search(keyword);
        return Result.success(products);
    }
}

为什么GET请求参数在URL中?

  • 便于缓存:相同URL的请求可以使用缓存
  • 便于分享:可以直接复制URL分享
  • 符合REST规范:资源定位应该通过URL
1.3.2 POST - 创建资源

特点:

  • 用于创建新资源
  • 参数在请求体中,不可见且无长度限制
  • 不可被缓存
  • 不可被收藏为书签
  • 非幂等性:多次请求会创建多个资源

使用场景:

// 1. 创建新商品
POST /api/products
Content-Type: application/json

{
  "name": "iPhone 15 Pro",
  "price": 7999.00,
  "category": "phone",
  "stock": 100
}

// 2. 用户注册
POST /api/users/register
Content-Type: application/json

{
  "username": "zhangsan",
  "password": "123456",
  "email": "zhangsan@example.com"
}

// 3. 用户登录
POST /api/users/login
Content-Type: application/json

{
  "username": "zhangsan",
  "password": "123456"
}

Java实现示例:

/**
 * 创建新商品
 * 
 * @param productDTO 商品信息
 * @return 创建的商品
 */
@PostMapping
public Result<Product> createProduct(@RequestBody @Valid ProductDTO productDTO) {
    // 1. 参数校验(使用@Valid注解自动校验)
    
    // 2. 转换DTO为实体
    Product product = new Product();
    product.setName(productDTO.getName());
    product.setPrice(productDTO.getPrice());
    product.setCategory(productDTO.getCategory());
    product.setStock(productDTO.getStock());
    product.setDescription(productDTO.getDescription());
    product.setCreateTime(new Date());
    
    // 3. 保存到数据库
    Product savedProduct = productService.save(product);
    
    // 4. 返回创建的资源(包含生成的ID)
    return Result.success(savedProduct);
}

ProductDTO数据传输对象:

package com.example.shop.dto;

import lombok.Data;
import javax.validation.constraints.*;
import java.math.BigDecimal;

/**
 * 商品DTO(数据传输对象)
 * 用于接收客户端传递的商品信息
 * 
 * @author 张三
 * @since 1.0.0
 */
@Data
public class ProductDTO {
    
    /**
     * 商品名称
     */
    @NotBlank(message = "商品名称不能为空")
    @Size(min = 2, max = 100, message = "商品名称长度必须在2-100之间")
    private String name;
    
    /**
     * 商品价格
     */
    @NotNull(message = "商品价格不能为空")
    @DecimalMin(value = "0.01", message = "商品价格必须大于0")
    @DecimalMax(value = "999999.99", message = "商品价格不能超过999999.99")
    private BigDecimal price;
    
    /**
     * 商品分类
     */
    @NotBlank(message = "商品分类不能为空")
    private String category;
    
    /**
     * 库存数量
     */
    @NotNull(message = "库存数量不能为空")
    @Min(value = 0, message = "库存数量不能为负数")
    private Integer stock;
    
    /**
     * 商品描述
     */
    @Size(max = 1000, message = "商品描述不能超过1000字")
    private String description;
}

为什么POST请求参数在请求体中?

  • 安全性:敏感信息不会出现在URL中
  • 容量:可以传输大量数据
  • 灵活性:支持多种数据格式(JSON、XML、表单等)
1.3.3 PUT - 完整更新资源

特点:

  • 用于完整更新资源(替换整个资源)
  • 参数在请求体中
  • 幂等性:多次请求结果相同
  • 必须提供资源的所有字段

使用场景:

// 完整更新商品信息(必须提供所有字段)
PUT /api/products/1
Content-Type: application/json

{
  "name": "iPhone 15 Pro Max",
  "price": 8999.00,
  "category": "phone",
  "stock": 50,
  "description": "最新款iPhone"
}

Java实现示例:

/**
 * 完整更新商品信息
 * 
 * @param id 商品ID
 * @param productDTO 商品完整信息
 * @return 更新后的商品
 */
@PutMapping("/{id}")
public Result<Product> updateProduct(
        @PathVariable Long id,
        @RequestBody @Valid ProductDTO productDTO) {
    
    // 1. 检查商品是否存在
    Product existingProduct = productService.findById(id);
    if (existingProduct == null) {
        return Result.error("商品不存在");
    }
    
    // 2. 完整更新所有字段
    existingProduct.setName(productDTO.getName());
    existingProduct.setPrice(productDTO.getPrice());
    existingProduct.setCategory(productDTO.getCategory());
    existingProduct.setStock(productDTO.getStock());
    existingProduct.setDescription(productDTO.getDescription());
    existingProduct.setUpdateTime(new Date());
    
    // 3. 保存更新
    Product updatedProduct = productService.update(existingProduct);
    
    return Result.success(updatedProduct);
}
1.3.4 PATCH - 部分更新资源

特点:

  • 用于部分更新资源(只更新指定字段)
  • 参数在请求体中
  • 幂等性:多次请求结果相同
  • 只需提供要更新的字段

使用场景:

// 只更新商品价格和库存
PATCH /api/products/1
Content-Type: application/json

{
  "price": 7499.00,
  "stock": 80
}

Java实现示例:

/**
 * 部分更新商品信息
 * 
 * @param id 商品ID
 * @param updates 要更新的字段(Map形式)
 * @return 更新后的商品
 */
@PatchMapping("/{id}")
public Result<Product> patchProduct(
        @PathVariable Long id,
        @RequestBody Map<String, Object> updates) {
    
    // 1. 检查商品是否存在
    Product product = productService.findById(id);
    if (product == null) {
        return Result.error("商品不存在");
    }
    
    // 2. 只更新提供的字段
    if (updates.containsKey("name")) {
        product.setName((String) updates.get("name"));
    }
    if (updates.containsKey("price")) {
        product.setPrice(new BigDecimal(updates.get("price").toString()));
    }
    if (updates.containsKey("stock")) {
        product.setStock((Integer) updates.get("stock"));
    }
    if (updates.containsKey("description")) {
        product.setDescription((String) updates.get("description"));
    }
    product.setUpdateTime(new Date());
    
    // 3. 保存更新
    Product updatedProduct = productService.update(product);
    
    return Result.success(updatedProduct);
}

PUT vs PATCH 对比:

维度 PUT PATCH
更新方式 完整更新(替换整个资源) 部分更新(只更新指定字段)
必须字段 必须提供所有字段 只需提供要更新的字段
幂等性 幂等 幂等
使用场景 表单提交、完整替换 单个字段修改、批量更新
示例 修改用户完整信息 修改用户头像、修改商品价格
1.3.5 DELETE - 删除资源

特点:

  • 用于删除资源
  • 通常没有请求体
  • 幂等性:多次删除同一资源结果相同
  • 删除后资源不再存在

使用场景:

// 1. 删除单个商品
DELETE /api/products/1

// 2. 批量删除商品
DELETE /api/products?ids=1,2,3

// 3. 删除用户的购物车项
DELETE /api/cart/items/1

Java实现示例:

/**
 * 删除商品
 * 
 * @param id 商品ID
 * @return 删除结果
 */
@DeleteMapping("/{id}")
public Result<Void> deleteProduct(@PathVariable Long id) {
    // 1. 检查商品是否存在
    Product product = productService.findById(id);
    if (product == null) {
        return Result.error("商品不存在");
    }
    
    // 2. 检查是否可以删除(业务规则)
    if (productService.hasOrders(id)) {
        return Result.error("该商品存在订单,无法删除");
    }
    
    // 3. 执行删除(软删除或硬删除)
    productService.delete(id);
    
    return Result.success();
}

/**
 * 批量删除商品
 * 
 * @param ids 商品ID列表
 * @return 删除结果
 */
@DeleteMapping
public Result<Void> batchDeleteProducts(@RequestParam List<Long> ids) {
    if (ids == null || ids.isEmpty()) {
        return Result.error("请选择要删除的商品");
    }
    
    // 批量删除
    productService.batchDelete(ids);
    
    return Result.success();
}

软删除 vs 硬删除:

// 硬删除:直接从数据库删除记录
public void hardDelete(Long id) {
    productDao.deleteById(id);
}

// 软删除:只标记为已删除,不真正删除(推荐)
public void softDelete(Long id) {
    Product product = productDao.findById(id);
    product.setDeleted(true);
    product.setDeleteTime(new Date());
    productDao.update(product);
}

为什么推荐软删除?

  • 数据可恢复:误删除可以恢复
  • 数据完整性:保留历史记录
  • 业务需求:可能需要查看已删除的数据
  • 审计追踪:记录删除操作
1.3.6 其他HTTP方法

HEAD - 获取资源元信息:

// 只获取响应头,不获取响应体
// 用于检查资源是否存在、获取资源大小等
HEAD /api/products/1

// 响应(只有响应头,没有响应体)
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 156
Last-Modified: Mon, 06 Jan 2024 10:00:00 GMT

OPTIONS - 获取支持的方法:

// 查询服务器支持哪些HTTP方法
OPTIONS /api/products

// 响应
HTTP/1.1 200 OK
Allow: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE

HTTP方法总结表:

方法 作用 幂等性 安全性 请求体 响应体
GET 获取资源
POST 创建资源
PUT 完整更新
PATCH 部分更新
DELETE 删除资源
HEAD 获取元信息
OPTIONS 获取支持的方法

幂等性说明:

幂等性:多次执行相同操作,结果相同

✓ 幂等:
- GET /api/products/1  (多次获取,结果相同)
- PUT /api/products/1  (多次更新为相同值,结果相同)
- DELETE /api/products/1  (多次删除,结果都是不存在)

✗ 非幂等:
- POST /api/products  (多次创建,会产生多个资源)

1.4 HTTP状态码详解

1.4.1 状态码分类
HTTP状态码由3位数字组成,分为5类:

1xx - 信息性状态码(Informational)
      请求已接收,继续处理

2xx - 成功状态码(Success)
      请求已成功处理

3xx - 重定向状态码(Redirection)
      需要进一步操作以完成请求

4xx - 客户端错误状态码(Client Error)
      请求有语法错误或无法实现

5xx - 服务器错误状态码(Server Error)
      服务器处理请求时发生错误
1.4.2 常用2xx成功状态码

200 OK - 请求成功:

// 使用场景:GET、PUT、PATCH请求成功
GET /api/products/1
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1,
  "name": "iPhone 15 Pro",
  "price": 7999.00
}

201 Created - 资源已创建:

// 使用场景:POST请求成功创建资源
POST /api/products
HTTP/1.1 201 Created
Location: /api/products/123
Content-Type: application/json

{
  "id": 123,
  "name": "iPhone 15 Pro",
  "price": 7999.00
}

204 No Content - 成功但无返回内容:

// 使用场景:DELETE请求成功
DELETE /api/products/1
HTTP/1.1 204 No Content

Java实现示例:

/**
 * 创建商品(返回201状态码)
 */
@PostMapping
@ResponseStatus(HttpStatus.CREATED)  // 返回201状态码
public Result<Product> createProduct(@RequestBody @Valid ProductDTO productDTO) {
    Product product = productService.save(productDTO);
    return Result.success(product);
}

/**
 * 删除商品(返回204状态码)
 */
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)  // 返回204状态码
public void deleteProduct(@PathVariable Long id) {
    productService.delete(id);
}

/**
 * 使用ResponseEntity自定义状态码
 */
@PostMapping("/custom")
public ResponseEntity<Result<Product>> createProductCustom(
        @RequestBody @Valid ProductDTO productDTO) {
    Product product = productService.save(productDTO);
    
    // 返回201状态码,并设置Location头
    return ResponseEntity
            .status(HttpStatus.CREATED)
            .header("Location", "/api/products/" + product.getId())
            .body(Result.success(product));
}
1.4.3 常用3xx重定向状态码

301 Moved Permanently - 永久重定向:

// 使用场景:资源永久移动到新位置
GET /api/old-products/1
HTTP/1.1 301 Moved Permanently
Location: /api/products/1

// 浏览器会自动跳转到新地址,并更新书签

302 Found - 临时重定向:

// 使用场景:资源临时移动
GET /api/products/1
HTTP/1.1 302 Found
Location: /api/temp-products/1

// 浏览器会跳转,但不更新书签

304 Not Modified - 资源未修改:

// 使用场景:客户端缓存验证
GET /api/products/1
If-Modified-Since: Mon, 06 Jan 2024 10:00:00 GMT

HTTP/1.1 304 Not Modified
// 无响应体,客户端使用缓存

Java实现示例:

/**
 * 重定向示例
 */
@GetMapping("/old-api/products/{id}")
public ResponseEntity<Void> redirectOldApi(@PathVariable Long id) {
    // 永久重定向到新API
    return ResponseEntity
            .status(HttpStatus.MOVED_PERMANENTLY)
            .header("Location", "/api/products/" + id)
            .build();
}

/**
 * 缓存验证示例
 */
@GetMapping("/{id}")
public ResponseEntity<Product> getProductWithCache(
        @PathVariable Long id,
        @RequestHeader(value = "If-Modified-Since", required = false) 
        String ifModifiedSince) {
    
    Product product = productService.findById(id);
    
    // 检查资源是否被修改
    if (ifModifiedSince != null) {
        Date clientCacheTime = parseDate(ifModifiedSince);
        if (!product.getUpdateTime().after(clientCacheTime)) {
            // 资源未修改,返回304
            return ResponseEntity
                    .status(HttpStatus.NOT_MODIFIED)
                    .build();
        }
    }
    
    // 资源已修改或首次请求,返回完整数据
    return ResponseEntity
            .ok()
            .header("Last-Modified", formatDate(product.getUpdateTime()))
            .body(product);
}
1.4.4 常用4xx客户端错误状态码

400 Bad Request - 请求参数错误:

// 使用场景:参数格式错误、缺少必填参数
POST /api/products
{
  "name": "",  // 名称为空
  "price": -100  // 价格为负数
}

HTTP/1.1 400 Bad Request
{
  "code": 400,
  "message": "参数校验失败",
  "errors": [
    "商品名称不能为空",
    "商品价格必须大于0"
  ]
}

401 Unauthorized - 未认证:

// 使用场景:未登录或Token无效
GET /api/user/profile

HTTP/1.1 401 Unauthorized
{
  "code": 401,
  "message": "请先登录"
}

403 Forbidden - 无权限:

// 使用场景:已登录但无权限访问
DELETE /api/products/1

HTTP/1.1 403 Forbidden
{
  "code": 403,
  "message": "您没有权限删除商品"
}

404 Not Found - 资源不存在:

// 使用场景:请求的资源不存在
GET /api/products/999

HTTP/1.1 404 Not Found
{
  "code": 404,
  "message": "商品不存在"
}

405 Method Not Allowed - 方法不允许:

// 使用场景:使用了不支持的HTTP方法
POST /api/products/1  // 应该使用PUT或PATCH

HTTP/1.1 405 Method Not Allowed
Allow: GET, PUT, PATCH, DELETE
{
  "code": 405,
  "message": "不支持POST方法"
}

409 Conflict - 资源冲突:

// 使用场景:资源状态冲突
POST /api/users/register
{
  "username": "zhangsan"  // 用户名已存在
}

HTTP/1.1 409 Conflict
{
  "code": 409,
  "message": "用户名已存在"
}

422 Unprocessable Entity - 语义错误:

// 使用场景:请求格式正确,但语义错误
POST /api/orders
{
  "productId": 1,
  "quantity": 1000  // 库存不足
}

HTTP/1.1 422 Unprocessable Entity
{
  "code": 422,
  "message": "库存不足,当前库存:100"
}

429 Too Many Requests - 请求过多:

// 使用场景:触发限流
GET /api/products

HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
  "code": 429,
  "message": "请求过于频繁,请60秒后重试"
}

Java实现示例:

/**
 * 全局异常处理器
 * 统一处理各种异常并返回对应的HTTP状态码
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 处理参数校验异常(400)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Void> handleValidationException(
            MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.toList());
        
        return Result.error(400, "参数校验失败", errors);
    }
    
    /**
     * 处理认证异常(401)
     */
    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Result<Void> handleAuthenticationException(
            AuthenticationException ex) {
        return Result.error(401, "请先登录");
    }
    
    /**
     * 处理权限异常(403)
     */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public Result<Void> handleAccessDeniedException(
            AccessDeniedException ex) {
        return Result.error(403, "您没有权限访问该资源");
    }
    
    /**
     * 处理资源不存在异常(404)
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Result<Void> handleResourceNotFoundException(
            ResourceNotFoundException ex) {
        return Result.error(404, ex.getMessage());
    }
    
    /**
     * 处理资源冲突异常(409)
     */
    @ExceptionHandler(ResourceConflictException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Result<Void> handleResourceConflictException(
            ResourceConflictException ex) {
        return Result.error(409, ex.getMessage());
    }
    
    /**
     * 处理业务异常(422)
     */
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public Result<Void> handleBusinessException(
            BusinessException ex) {
        return Result.error(422, ex.getMessage());
    }
    
    /**
     * 处理限流异常(429)
     */
    @ExceptionHandler(RateLimitException.class)
    public ResponseEntity<Result<Void>> handleRateLimitException(
            RateLimitException ex) {
        return ResponseEntity
                .status(HttpStatus.TOO_MANY_REQUESTS)
                .header("Retry-After", "60")
                .body(Result.error(429, "请求过于频繁,请稍后重试"));
    }
}

自定义异常类:

/**
 * 资源不存在异常
 */
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

/**
 * 资源冲突异常
 */
public class ResourceConflictException extends RuntimeException {
    public ResourceConflictException(String message) {
        super(message);
    }
}

/**
 * 业务异常
 */
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

/**
 * 限流异常
 */
public class RateLimitException extends RuntimeException {
    public RateLimitException(String message) {
        super(message);
    }
}
1.4.5 常用5xx服务器错误状态码

500 Internal Server Error - 服务器内部错误:

// 使用场景:服务器代码异常
GET /api/products/1

HTTP/1.1 500 Internal Server Error
{
  "code": 500,
  "message": "服务器内部错误,请稍后重试"
}

502 Bad Gateway - 网关错误:

// 使用场景:网关或代理服务器从上游服务器收到无效响应
GET /api/products/1

HTTP/1.1 502 Bad Gateway
{
  "code": 502,
  "message": "网关错误"
}

503 Service Unavailable - 服务不可用:

// 使用场景:服务器维护或过载
GET /api/products/1

HTTP/1.1 503 Service Unavailable
Retry-After: 3600
{
  "code": 503,
  "message": "服务暂时不可用,请稍后重试"
}

504 Gateway Timeout - 网关超时:

// 使用场景:网关或代理服务器等待上游服务器响应超时
GET /api/products/1

HTTP/1.1 504 Gateway Timeout
{
  "code": 504,
  "message": "请求超时"
}

Java实现示例:

/**
 * 处理服务器异常(500)
 */
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleException(Exception ex) {
    // 记录详细错误日志
    log.error("服务器内部错误", ex);
    
    // 返回通用错误信息(不暴露内部细节)
    return Result.error(500, "服务器内部错误,请稍后重试");
}

/**
 * 处理服务不可用异常(503)
 */
@ExceptionHandler(ServiceUnavailableException.class)
public ResponseEntity<Result<Void>> handleServiceUnavailableException(
        ServiceUnavailableException ex) {
    return ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .header("Retry-After", "3600")
            .body(Result.error(503, "服务暂时不可用,请稍后重试"));
}

HTTP状态码选择流程图:

需要且未认证

已认证

不需要

无权限

有权限

无效

有效

不存在

存在

冲突

语义错误

成功

服务器错误

GET/PUT/PATCH

POST

DELETE

收到请求

是否需要认证?

返回401 Unauthorized

是否有权限?

参数是否有效?

返回403 Forbidden

返回400 Bad Request

资源是否存在?

返回404 Not Found

业务逻辑是否成功?

返回409 Conflict

返回422 Unprocessable Entity

请求方法?

返回500 Internal Server Error

返回200 OK

返回201 Created

返回204 No Content

HTTP状态码总结表:

状态码 含义 使用场景 示例
200 OK 请求成功 GET、PUT、PATCH成功
201 Created 资源已创建 POST创建成功
204 No Content 成功但无内容 DELETE成功
301 Moved Permanently 永久重定向 API版本升级
302 Found 临时重定向 临时维护页面
304 Not Modified 资源未修改 缓存验证
400 Bad Request 请求参数错误 参数格式错误
401 Unauthorized 未认证 未登录
403 Forbidden 无权限 权限不足
404 Not Found 资源不存在 商品不存在
405 Method Not Allowed 方法不允许 使用错误的HTTP方法
409 Conflict 资源冲突 用户名已存在
422 Unprocessable Entity 语义错误 库存不足
429 Too Many Requests 请求过多 触发限流
500 Internal Server Error 服务器错误 代码异常
502 Bad Gateway 网关错误 上游服务异常
503 Service Unavailable 服务不可用 服务器维护
504 Gateway Timeout 网关超时 请求超时

1.5 HTTP请求头和响应头详解

1.5.1 常用请求头(Request Headers)

Content-Type - 请求体数据类型:

# JSON格式(最常用)
Content-Type: application/json

{
  "name": "iPhone 15 Pro",
  "price": 7999.00
}

# 表单格式
Content-Type: application/x-www-form-urlencoded

name=iPhone+15+Pro&price=7999.00

# 文件上传
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="image.jpg"
Content-Type: image/jpeg

[文件二进制数据]
------WebKitFormBoundary--

# XML格式
Content-Type: application/xml

<product>
  <name>iPhone 15 Pro</name>
  <price>7999.00</price>
</product>

Authorization - 认证信息:

# Bearer Token(JWT)
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# Basic认证
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

# API Key
Authorization: ApiKey your-api-key-here

Accept - 客户端接受的数据类型:

# 接受JSON
Accept: application/json

# 接受XML
Accept: application/xml

# 接受多种类型(按优先级)
Accept: application/json, application/xml;q=0.9, */*;q=0.8

User-Agent - 客户端信息:

# 浏览器
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36

# 移动App
User-Agent: MyShopApp/1.0.0 (iOS 17.0; iPhone 15 Pro)

# API客户端
User-Agent: PostmanRuntime/7.32.0

Cookie - 客户端Cookie:

Cookie: sessionId=abc123; userId=1; theme=dark

Referer - 来源页面:

Referer: https://www.example.com/products

Cache-Control - 缓存控制:

# 不使用缓存
Cache-Control: no-cache

# 不存储缓存
Cache-Control: no-store

# 使用缓存(最多1小时)
Cache-Control: max-age=3600

If-Modified-Since - 条件请求:

If-Modified-Since: Mon, 06 Jan 2024 10:00:00 GMT

If-None-Match - ETag条件请求:

If-None-Match: "686897696a7c876b7e"

Java处理请求头示例:

/**
 * 处理各种请求头
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    /**
     * 获取请求头信息
     */
    @GetMapping("/{id}")
    public Result<Product> getProduct(
            @PathVariable Long id,
            @RequestHeader("Authorization") String authorization,
            @RequestHeader(value = "User-Agent", required = false) String userAgent,
            @RequestHeader(value = "Accept", defaultValue = "application/json") String accept,
            HttpServletRequest request) {
        
        // 1. 验证Token
        String token = authorization.replace("Bearer ", "");
        if (!jwtUtil.validateToken(token)) {
            throw new AuthenticationException("Token无效");
        }
        
        // 2. 记录用户代理信息
        log.info("User-Agent: {}", userAgent);
        
        // 3. 根据Accept返回不同格式
        Product product = productService.findById(id);
        
        if (accept.contains("application/xml")) {
            // 返回XML格式
            return Result.success(product);
        } else {
            // 返回JSON格式(默认)
            return Result.success(product);
        }
    }
    
    /**
     * 处理文件上传
     */
    @PostMapping("/upload")
    public Result<String> uploadImage(
            @RequestParam("file") MultipartFile file,
            @RequestHeader("Content-Type") String contentType) {
        
        // 1. 检查Content-Type
        if (!contentType.startsWith("multipart/form-data")) {
            return Result.error("Content-Type必须是multipart/form-data");
        }
        
        // 2. 检查文件类型
        String originalFilename = file.getOriginalFilename();
        if (!isImageFile(originalFilename)) {
            return Result.error("只支持图片文件");
        }
        
        // 3. 保存文件
        String url = fileService.save(file);
        
        return Result.success(url);
    }
    
    /**
     * 支持缓存的接口
     */
    @GetMapping("/{id}/cached")
    public ResponseEntity<Product> getProductCached(
            @PathVariable Long id,
            @RequestHeader(value = "If-Modified-Since", required = false) 
            String ifModifiedSince,
            @RequestHeader(value = "If-None-Match", required = false) 
            String ifNoneMatch) {
        
        Product product = productService.findById(id);
        
        // 生成ETag(基于内容的哈希值)
        String etag = generateETag(product);
        
        // 检查ETag
        if (ifNoneMatch != null && ifNoneMatch.equals(etag)) {
            return ResponseEntity
                    .status(HttpStatus.NOT_MODIFIED)
                    .eTag(etag)
                    .build();
        }
        
        // 检查Last-Modified
        if (ifModifiedSince != null) {
            Date clientCacheTime = parseDate(ifModifiedSince);
            if (!product.getUpdateTime().after(clientCacheTime)) {
                return ResponseEntity
                        .status(HttpStatus.NOT_MODIFIED)
                        .lastModified(product.getUpdateTime().getTime())
                        .build();
            }
        }
        
        // 返回完整数据
        return ResponseEntity
                .ok()
                .eTag(etag)
                .lastModified(product.getUpdateTime().getTime())
                .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
                .body(product);
    }
}
1.5.2 常用响应头(Response Headers)

Content-Type - 响应体数据类型:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "id": 1,
  "name": "iPhone 15 Pro"
}

Content-Length - 响应体长度:

HTTP/1.1 200 OK
Content-Length: 156

Set-Cookie - 设置Cookie:

HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; Path=/; HttpOnly; Secure; Max-Age=3600
Set-Cookie: userId=1; Path=/; Max-Age=86400

Location - 资源位置:

HTTP/1.1 201 Created
Location: /api/products/123

Cache-Control - 缓存策略:

# 不缓存
Cache-Control: no-cache, no-store, must-revalidate

# 缓存1小时
Cache-Control: public, max-age=3600

# 私有缓存(只能浏览器缓存,不能CDN缓存)
Cache-Control: private, max-age=3600

ETag - 资源标识:

HTTP/1.1 200 OK
ETag: "686897696a7c876b7e"

Last-Modified - 最后修改时间:

HTTP/1.1 200 OK
Last-Modified: Mon, 06 Jan 2024 10:00:00 GMT

Access-Control-Allow-Origin - CORS跨域:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Retry-After - 重试时间:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

Java设置响应头示例:

/**
 * 设置各种响应头
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    /**
     * 设置Cookie
     */
    @PostMapping("/login")
    public ResponseEntity<Result<String>> login(
            @RequestBody LoginDTO loginDTO,
            HttpServletResponse response) {
        
        // 验证用户名密码
        User user = userService.login(loginDTO);
        
        // 生成Token
        String token = jwtUtil.generateToken(user);
        
        // 设置Cookie
        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setHttpOnly(true);  // 防止XSS攻击
        cookie.setSecure(true);    // 只在HTTPS下传输
        cookie.setMaxAge(3600);    // 1小时过期
        response.addCookie(cookie);
        
        return ResponseEntity.ok(Result.success(token));
    }
    
    /**
     * 设置缓存头
     */
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        Product product = productService.findById(id);
        
        return ResponseEntity
                .ok()
                .cacheControl(CacheControl
                        .maxAge(1, TimeUnit.HOURS)
                        .cachePublic())
                .eTag(generateETag(product))
                .lastModified(product.getUpdateTime().getTime())
                .body(product);
    }
    
    /**
     * 设置CORS头
     */
    @CrossOrigin(
        origins = "https://www.example.com",
        methods = {RequestMethod.GET, RequestMethod.POST},
        allowedHeaders = {"Content-Type", "Authorization"},
        maxAge = 3600
    )
    @GetMapping
    public Result<List<Product>> getProducts() {
        List<Product> products = productService.findAll();
        return Result.success(products);
    }
    
    /**
     * 全局CORS配置
     */
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
        
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/api/**")
                    .allowedOrigins("https://www.example.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
                    .allowedHeaders("*")
                    .allowCredentials(true)
                    .maxAge(3600);
        }
    }
    
    /**
     * 设置限流响应头
     */
    @GetMapping("/limited")
    public ResponseEntity<Result<List<Product>>> getProductsLimited() {
        // 检查限流
        if (rateLimiter.isLimited()) {
            return ResponseEntity
                    .status(HttpStatus.TOO_MANY_REQUESTS)
                    .header("Retry-After", "60")
                    .header("X-RateLimit-Limit", "100")
                    .header("X-RateLimit-Remaining", "0")
                    .header("X-RateLimit-Reset", String.valueOf(
                        System.currentTimeMillis() + 60000))
                    .body(Result.error(429, "请求过于频繁"));
        }
        
        List<Product> products = productService.findAll();
        
        return ResponseEntity
                .ok()
                .header("X-RateLimit-Limit", "100")
                .header("X-RateLimit-Remaining", "99")
                .body(Result.success(products));
    }
}

HTTP请求头和响应头总结表:

类型 Header 作用 示例
请求头 Content-Type 请求体数据类型 application/json
请求头 Authorization 认证信息 Bearer token
请求头 Accept 接受的数据类型 application/json
请求头 User-Agent 客户端信息 Mozilla/5.0
请求头 Cookie 客户端Cookie sessionId=abc123
请求头 Referer 来源页面 https://example.com
请求头 Cache-Control 缓存控制 no-cache
请求头 If-Modified-Since 条件请求 Mon, 06 Jan 2024
请求头 If-None-Match ETag条件请求 “686897696a7c876b7e”
响应头 Content-Type 响应体数据类型 application/json
响应头 Content-Length 响应体长度 156
响应头 Set-Cookie 设置Cookie sessionId=abc123
响应头 Location 资源位置 /api/products/123
响应头 Cache-Control 缓存策略 max-age=3600
响应头 ETag 资源标识 “686897696a7c876b7e”
响应头 Last-Modified 最后修改时间 Mon, 06 Jan 2024
响应头 Access-Control-Allow-Origin CORS跨域 *
响应头 Retry-After 重试时间 60

1.6 HTTPS加密原理

1.6.1 为什么需要HTTPS?

HTTP的安全问题:

场景:用户在咖啡厅使用公共WiFi登录网站

HTTP传输过程:
客户端 → 明文传输 → WiFi路由器 → 明文传输 → 服务器
         ↑
    黑客可以截获
    用户名:zhangsan
    密码:123456
    
问题:
1. 数据明文传输,容易被窃听
2. 数据可能被篡改
3. 无法验证服务器身份(可能是钓鱼网站)

HTTPS的解决方案:

HTTPS = HTTP + SSL/TLS

客户端 → 加密传输 → WiFi路由器 → 加密传输 → 服务器
         ↑
    黑客只能看到乱码
    无法解密
    
优势:
1. 数据加密传输,防止窃听
2. 数据完整性校验,防止篡改
3. 服务器身份认证,防止钓鱼
1.6.2 HTTPS加密流程
服务器 客户端 服务器 客户端 1. TCP三次握手 2. TLS握手 3. 验证证书 4. 生成随机密钥 5. 加密通信 SYN SYN-ACK ACK Client Hello (支持的加密算法) Server Hello (选择的加密算法) Certificate (服务器证书) Server Hello Done Client Key Exchange (用服务器公钥加密的随机密钥) Change Cipher Spec (开始使用加密通信) Finished (握手完成) Change Cipher Spec Finished 加密的HTTP请求 加密的HTTP响应

详细步骤说明:

步骤1:客户端发起请求

Client Hello消息包含:
- TLS版本(如TLS 1.3)
- 支持的加密算法列表
- 随机数1(用于后续生成密钥)

步骤2:服务器响应

Server Hello消息包含:
- 选择的TLS版本
- 选择的加密算法
- 随机数2(用于后续生成密钥)
- 服务器证书(包含公钥)

步骤3:客户端验证证书

验证内容:
1. 证书是否过期
2. 证书域名是否匹配
3. 证书是否被信任的CA签发
4. 证书是否被吊销

验证通过后,提取服务器公钥

步骤4:生成会话密钥

客户端:
1. 生成随机数3(Pre-Master Secret)
2. 使用服务器公钥加密随机数3
3. 发送给服务器

服务器:
1. 使用私钥解密,得到随机数3

双方:
使用随机数1、随机数2、随机数3生成相同的会话密钥

步骤5:加密通信

使用会话密钥进行对称加密通信
- 加密速度快
- 安全性高
1.6.3 对称加密 vs 非对称加密

对称加密:

加密和解密使用相同的密钥

加密:明文 + 密钥 → 密文
解密:密文 + 密钥 → 明文

优点:速度快
缺点:密钥传输不安全

常见算法:AES、DES、3DES

非对称加密:

加密和解密使用不同的密钥(公钥和私钥)

加密:明文 + 公钥 → 密文
解密:密文 + 私钥 → 明文

优点:密钥传输安全
缺点:速度慢

常见算法:RSA、ECC

HTTPS混合加密:

1. 使用非对称加密传输会话密钥(安全)
2. 使用对称加密传输数据(快速)

兼顾了安全性和性能

Java实现HTTPS示例:

1. 生成SSL证书(开发环境):

# 使用keytool生成自签名证书
keytool -genkeypair -alias myshop -keyalg RSA -keysize 2048 \
  -storetype PKCS12 -keystore keystore.p12 -validity 3650 \
  -storepass 123456

# 参数说明:
# -alias: 证书别名
# -keyalg: 加密算法
# -keysize: 密钥长度
# -keystore: 证书文件名
# -validity: 有效期(天)
# -storepass: 证书密码

2. Spring Boot配置HTTPS:

# application.yml
server:
  port: 8443  # HTTPS端口
  ssl:
    enabled: true
    key-store: classpath:keystore.p12  # 证书路径
    key-store-password: 123456          # 证书密码
    key-store-type: PKCS12              # 证书类型
    key-alias: myshop                   # 证书别名

3. 强制使用HTTPS:

package com.example.shop.config;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.catalina.connector.Connector;

/**
 * HTTPS配置
 * 
 * @author 张三
 * @since 1.0.0
 */
@Configuration
public class HttpsConfig {
    
    /**
     * 配置HTTP自动跳转到HTTPS
     */
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> 
            servletContainer() {
        return factory -> {
            // 添加HTTP连接器
            Connector connector = new Connector(
                "org.apache.coyote.http11.Http11NioProtocol");
            connector.setScheme("http");
            connector.setPort(8080);  // HTTP端口
            connector.setSecure(false);
            connector.setRedirectPort(8443);  // 重定向到HTTPS端口
            
            factory.addAdditionalTomcatConnectors(connector);
        };
    }
}

4. 使用RestTemplate访问HTTPS:

package com.example.shop.util;

import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.*;
import java.security.cert.X509Certificate;

/**
 * HTTPS工具类
 * 
 * @author 张三
 * @since 1.0.0
 */
public class HttpsUtil {
    
    /**
     * 创建信任所有证书的RestTemplate(仅用于开发环境)
     */
    public static RestTemplate createTrustAllRestTemplate() {
        try {
            // 创建信任所有证书的TrustManager
            TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }
                    public void checkClientTrusted(
                        X509Certificate[] certs, String authType) {
                    }
                    public void checkServerTrusted(
                        X509Certificate[] certs, String authType) {
                    }
                }
            };
            
            // 安装信任所有证书的TrustManager
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCerts, 
                new java.security.SecureRandom());
            
            // 创建HttpsURLConnection
            HttpsURLConnection.setDefaultSSLSocketFactory(
                sslContext.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(
                (hostname, session) -> true);
            
            // 创建RestTemplate
            SimpleClientHttpRequestFactory factory = 
                new SimpleClientHttpRequestFactory();
            factory.setConnectTimeout(5000);
            factory.setReadTimeout(5000);
            
            return new RestTemplate(factory);
            
        } catch (Exception e) {
            throw new RuntimeException("创建RestTemplate失败", e);
        }
    }
    
    /**
     * 使用示例
     */
    public static void main(String[] args) {
        RestTemplate restTemplate = createTrustAllRestTemplate();
        
        // 访问HTTPS接口
        String url = "https://localhost:8443/api/products/1";
        Product product = restTemplate.getForObject(url, Product.class);
        
        System.out.println(product);
    }
}

5. 生产环境使用正式证书:

/**
 * 生产环境HTTPS配置
 * 使用Let's Encrypt免费证书或购买商业证书
 */
@Configuration
public class ProductionHttpsConfig {
    
    @Value("${server.ssl.key-store}")
    private String keyStore;
    
    @Value("${server.ssl.key-store-password}")
    private String keyStorePassword;
    
    /**
     * 配置SSL
     */
    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = 
            new TomcatServletWebServerFactory();
        
        tomcat.addConnectorCustomizers(connector -> {
            connector.setScheme("https");
            connector.setSecure(true);
            connector.setPort(443);  // HTTPS标准端口
        });
        
        return tomcat;
    }
}

HTTPS最佳实践:

/**
 * HTTPS安全配置最佳实践
 */
@Configuration
public class SecurityConfig {
    
    /**
     * 1. 强制使用HTTPS
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) 
            throws Exception {
        http
            .requiresChannel()
            .anyRequest()
            .requiresSecure();  // 强制HTTPS
        
        return http.build();
    }
    
    /**
     * 2. 配置HSTS(HTTP Strict Transport Security)
     * 告诉浏览器只能通过HTTPS访问
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/public/**");
    }
    
    /**
     * 3. 配置安全响应头
     */
    @Bean
    public FilterRegistrationBean<SecurityHeadersFilter> 
            securityHeadersFilter() {
        FilterRegistrationBean<SecurityHeadersFilter> registration = 
            new FilterRegistrationBean<>();
        registration.setFilter(new SecurityHeadersFilter());
        registration.addUrlPatterns("/*");
        return registration;
    }
}

/**
 * 安全响应头过滤器
 */
public class SecurityHeadersFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                        FilterChain chain) throws IOException, ServletException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // HSTS:强制使用HTTPS(1年)
        httpResponse.setHeader("Strict-Transport-Security",
            "max-age=31536000; includeSubDomains");
        
        // X-Frame-Options:防止点击劫持
        httpResponse.setHeader("X-Frame-Options", "DENY");
        
        // X-Content-Type-Options:防止MIME类型嗅探
        httpResponse.setHeader("X-Content-Type-Options", "nosniff");
        
        // X-XSS-Protection:启用XSS过滤
        httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
        
        // Content-Security-Policy:内容安全策略
        httpResponse.setHeader("Content-Security-Policy",
            "default-src 'self'");
        
        chain.doFilter(request, response);
    }
}

HTTP vs HTTPS对比:

维度 HTTP HTTPS
安全性 明文传输,不安全 加密传输,安全
端口 80 443
证书 不需要 需要SSL证书
性能 稍慢(加密解密开销)
SEO 普通 搜索引擎优先
浏览器提示 “不安全” 显示锁图标
成本 免费 证书可能需要付费
适用场景 公开信息 敏感信息、登录、支付

二、RESTful API设计原则

2.1 什么是RESTful?

REST(Representational State Transfer)表述性状态转移

REST是一种软件架构风格,不是标准或协议
由Roy Fielding在2000年博士论文中提出

为什么需要RESTful?

传统API设计的问题:

// 传统API:动词导向,URL混乱
GET  /getUserById?id=1
POST /createUser
POST /updateUser
POST /deleteUser?id=1
GET  /searchUsers?name=zhang

// RESTful API:资源导向,URL清晰
GET    /users/1        # 获取用户
POST   /users          # 创建用户
PUT    /users/1        # 更新用户
DELETE /users/1        # 删除用户
GET    /users?name=zhang  # 搜索用户

优势:
1. URL更简洁清晰
2. 语义明确
3. 易于理解和维护
4. 符合HTTP协议设计

2.2 RESTful核心原则

原则1:资源(Resource)

/**
 * 资源是REST的核心概念
 * 一切皆资源:用户、商品、订单等
 * 每个资源都有唯一的URI
 */

// ✅ 正确:资源导向
GET /products          // 商品列表
GET /products/123      // 单个商品
GET /products/123/reviews  // 商品的评论

// ❌ 错误:动词导向
GET /getProducts
GET /getProductById?id=123
GET /getProductReviews?productId=123

原则2:统一接口(Uniform Interface)

/**
 * 使用标准的HTTP方法操作资源
 * GET:查询
 * POST:创建
 * PUT:完整更新
 * PATCH:部分更新
 * DELETE:删除
 */

// 商品资源的CRUD操作
GET    /products       // 查询商品列表
GET    /products/123   // 查询单个商品
POST   /products       // 创建商品
PUT    /products/123   // 完整更新商品
PATCH  /products/123   // 部分更新商品
DELETE /products/123   // 删除商品

原则3:无状态(Stateless)

/**
 * 服务器不保存客户端状态
 * 每个请求包含所有必要信息
 * 通过Token等方式传递身份信息
 */

// ✅ 正确:请求包含所有必要信息
GET /products/123
Headers:
  Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
  Accept: application/json

// ❌ 错误:依赖服务器session
GET /products/123
// 依赖服务器端的session来识别用户

原则4:可缓存(Cacheable)

/**
 * 响应应该明确标识是否可缓存
 * 提高性能,减少服务器负载
 */

@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
    Product product = productService.findById(id);
    
    return ResponseEntity.ok()
        .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
        .eTag(String.valueOf(product.getVersion()))
        .body(product);
}

2.3 RESTful URL设计规范

规范1:使用名词,不使用动词

// ✅ 正确
GET    /users
POST   /users
GET    /users/123
PUT    /users/123
DELETE /users/123

// ❌ 错误
GET    /getUsers
POST   /createUser
GET    /getUserById
POST   /updateUser
POST   /deleteUser

规范2:使用复数形式

// ✅ 正确:使用复数
GET /products
GET /orders
GET /users

// ❌ 错误:使用单数
GET /product
GET /order
GET /user

规范3:使用小写字母和连字符

// ✅ 正确
GET /product-categories
GET /user-addresses
GET /order-items

// ❌ 错误
GET /ProductCategories
GET /user_addresses
GET /orderItems

规范4:表示层级关系

// 用户的订单
GET /users/123/orders

// 订单的商品
GET /orders/456/items

// 商品的评论
GET /products/789/reviews

// 评论的回复
GET /reviews/111/replies

规范5:使用查询参数过滤

// 分页
GET /products?page=1&size=20

// 排序
GET /products?sort=price,desc

// 过滤
GET /products?category=electronics&minPrice=100&maxPrice=1000

// 搜索
GET /products?q=iPhone

// 组合使用
GET /products?category=electronics&sort=price,asc&page=1&size=20

2.4 RESTful响应设计

统一响应格式:

/**
 * 统一响应结果类
 */
@Data
public class ApiResponse<T> {
    private int code;           // 业务状态码
    private String message;     // 提示信息
    private T data;             // 数据
    private Long timestamp;     // 时间戳
    
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("成功");
        response.setData(data);
        response.setTimestamp(System.currentTimeMillis());
        return response;
    }
    
    public static <T> ApiResponse<T> error(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMessage(message);
        response.setTimestamp(System.currentTimeMillis());
        return response;
    }
}

/**
 * 使用示例
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @GetMapping("/{id}")
    public ApiResponse<Product> getProduct(@PathVariable Long id) {
        Product product = productService.findById(id);
        return ApiResponse.success(product);
    }
    
    @GetMapping
    public ApiResponse<List<Product>> getProducts() {
        List<Product> products = productService.findAll();
        return ApiResponse.success(products);
    }
}

分页响应格式:

/**
 * 分页响应类
 */
@Data
public class PageResponse<T> {
    private List<T> items;      // 数据列表
    private long total;         // 总数
    private int page;           // 当前页
    private int size;           // 每页大小
    private int totalPages;     // 总页数
    
    public PageResponse(List<T> items, long total, int page, int size) {
        this.items = items;
        this.total = total;
        this.page = page;
        this.size = size;
        this.totalPages = (int) Math.ceil((double) total / size);
    }
}

/**
 * 使用示例
 */
@GetMapping
public ApiResponse<PageResponse<Product>> getProducts(
        @RequestParam(defaultValue = "1") int page,
        @RequestParam(defaultValue = "20") int size) {
    
    Page<Product> productPage = productService.findAll(page, size);
    
    PageResponse<Product> pageResponse = new PageResponse<>(
        productPage.getContent(),
        productPage.getTotalElements(),
        page,
        size
    );
    
    return ApiResponse.success(pageResponse);
}

三、电商项目商品API设计实战

3.1 商品API需求分析

功能需求:

1. 商品列表查询(支持分页、排序、筛选)
2. 商品详情查询
3. 商品创建(管理员)
4. 商品更新(管理员)
5. 商品删除(管理员)
6. 商品搜索
7. 商品分类查询
8. 商品库存查询

3.2 商品实体设计

/**
 * 商品实体类
 */
@Data
@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 200)
    private String name;
    
    @Column(columnDefinition = "TEXT")
    private String description;
    
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;
    
    @Column(nullable = false)
    private Integer stock;
    
    @Column(length = 500)
    private String imageUrl;
    
    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;
    
    @Column(nullable = false)
    private ProductStatus status;
    
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt;
}

/**
 * 商品状态枚举
 */
public enum ProductStatus {
    DRAFT,      // 草稿
    PUBLISHED,  // 已发布
    SOLD_OUT,   // 已售罄
    ARCHIVED    // 已归档
}

/**
 * 商品分类实体
 */
@Data
@Entity
@Table(name = "categories")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 100)
    private String name;
    
    @Column(length = 500)
    private String description;
    
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent;
    
    @OneToMany(mappedBy = "parent")
    private List<Category> children;
}

3.3 商品DTO设计

/**
 * 商品创建DTO
 */
@Data
public class ProductCreateDTO {
    @NotBlank(message = "商品名称不能为空")
    @Size(max = 200, message = "商品名称不能超过200个字符")
    private String name;
    
    @Size(max = 2000, message = "商品描述不能超过2000个字符")
    private String description;
    
    @NotNull(message = "商品价格不能为空")
    @DecimalMin(value = "0.01", message = "商品价格必须大于0")
    private BigDecimal price;
    
    @NotNull(message = "商品库存不能为空")
    @Min(value = 0, message = "商品库存不能为负数")
    private Integer stock;
    
    private String imageUrl;
    
    @NotNull(message = "商品分类不能为空")
    private Long categoryId;
}

/**
 * 商品更新DTO
 */
@Data
public class ProductUpdateDTO {
    @Size(max = 200, message = "商品名称不能超过200个字符")
    private String name;
    
    @Size(max = 2000, message = "商品描述不能超过2000个字符")
    private String description;
    
    @DecimalMin(value = "0.01", message = "商品价格必须大于0")
    private BigDecimal price;
    
    @Min(value = 0, message = "商品库存不能为负数")
    private Integer stock;
    
    private String imageUrl;
    
    private Long categoryId;
    
    private ProductStatus status;
}

/**
 * 商品查询DTO
 */
@Data
public class ProductQueryDTO {
    private String keyword;         // 关键词搜索
    private Long categoryId;        // 分类ID
    private BigDecimal minPrice;    // 最低价格
    private BigDecimal maxPrice;    // 最高价格
    private ProductStatus status;   // 商品状态
    private String sortBy;          // 排序字段
    private String sortOrder;       // 排序方向
    private Integer page = 1;       // 页码
    private Integer size = 20;      // 每页大小
}

/**
 * 商品响应DTO
 */
@Data
public class ProductResponseDTO {
    private Long id;
    private String name;
    private String description;
    private BigDecimal price;
    private Integer stock;
    private String imageUrl;
    private CategoryDTO category;
    private ProductStatus status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

/**
 * 分类DTO
 */
@Data
public class CategoryDTO {
    private Long id;
    private String name;
    private String description;
}

3.4 商品Controller实现

/**
 * 商品控制器
 */
@RestController
@RequestMapping("/api/products")
@Validated
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    /**
     * 查询商品列表
     */
    @GetMapping
    public ApiResponse<PageResponse<ProductResponseDTO>> getProducts(
            @Valid ProductQueryDTO queryDTO) {
        
        PageResponse<ProductResponseDTO> products = 
            productService.findProducts(queryDTO);
        
        return ApiResponse.success(products);
    }
    
    /**
     * 查询商品详情
     */
    @GetMapping("/{id}")
    public ApiResponse<ProductResponseDTO> getProduct(@PathVariable Long id) {
        ProductResponseDTO product = productService.findById(id);
        
        if (product == null) {
            return ApiResponse.error(404, "商品不存在");
        }
        
        return ApiResponse.success(product);
    }
    
    /**
     * 创建商品
     */
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<ProductResponseDTO> createProduct(
            @Valid @RequestBody ProductCreateDTO createDTO) {
        
        ProductResponseDTO product = productService.create(createDTO);
        return ApiResponse.success(product);
    }
    
    /**
     * 更新商品
     */
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<ProductResponseDTO> updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody ProductUpdateDTO updateDTO) {
        
        ProductResponseDTO product = productService.update(id, updateDTO);
        
        if (product == null) {
            return ApiResponse.error(404, "商品不存在");
        }
        
        return ApiResponse.success(product);
    }
    
    /**
     * 删除商品
     */
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<Void> deleteProduct(@PathVariable Long id) {
        boolean deleted = productService.delete(id);
        
        if (!deleted) {
            return ApiResponse.error(404, "商品不存在");
        }
        
        return ApiResponse.success(null);
    }
    
    /**
     * 搜索商品
     */
    @GetMapping("/search")
    public ApiResponse<PageResponse<ProductResponseDTO>> searchProducts(
            @RequestParam String q,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        PageResponse<ProductResponseDTO> products = 
            productService.search(q, page, size);
        
        return ApiResponse.success(products);
    }
    
    /**
     * 查询商品库存
     */
    @GetMapping("/{id}/stock")
    public ApiResponse<Integer> getProductStock(@PathVariable Long id) {
        Integer stock = productService.getStock(id);
        
        if (stock == null) {
            return ApiResponse.error(404, "商品不存在");
        }
        
        return ApiResponse.success(stock);
    }
}

3.5 商品Service实现

/**
 * 商品服务接口
 */
public interface ProductService {
    PageResponse<ProductResponseDTO> findProducts(ProductQueryDTO queryDTO);
    ProductResponseDTO findById(Long id);
    ProductResponseDTO create(ProductCreateDTO createDTO);
    ProductResponseDTO update(Long id, ProductUpdateDTO updateDTO);
    boolean delete(Long id);
    PageResponse<ProductResponseDTO> search(String keyword, int page, int size);
    Integer getStock(Long id);
}

/**
 * 商品服务实现
 */
@Service
@Transactional
public class ProductServiceImpl implements ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private CategoryRepository categoryRepository;
    
    @Override
    @Transactional(readOnly = true)
    public PageResponse<ProductResponseDTO> findProducts(ProductQueryDTO queryDTO) {
        // 构建查询条件
        Specification<Product> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            // 关键词搜索
            if (StringUtils.hasText(queryDTO.getKeyword())) {
                String keyword = "%" + queryDTO.getKeyword() + "%";
                predicates.add(cb.or(
                    cb.like(root.get("name"), keyword),
                    cb.like(root.get("description"), keyword)
                ));
            }
            
            // 分类筛选
            if (queryDTO.getCategoryId() != null) {
                predicates.add(cb.equal(
                    root.get("category").get("id"), 
                    queryDTO.getCategoryId()
                ));
            }
            
            // 价格范围
            if (queryDTO.getMinPrice() != null) {
                predicates.add(cb.greaterThanOrEqualTo(
                    root.get("price"), 
                    queryDTO.getMinPrice()
                ));
            }
            if (queryDTO.getMaxPrice() != null) {
                predicates.add(cb.lessThanOrEqualTo(
                    root.get("price"), 
                    queryDTO.getMaxPrice()
                ));
            }
            
            // 状态筛选
            if (queryDTO.getStatus() != null) {
                predicates.add(cb.equal(
                    root.get("status"), 
                    queryDTO.getStatus()
                ));
            }
            
            return cb.and(predicates.toArray(new Predicate[0]));
        };
        
        // 构建排序
        Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
        if (StringUtils.hasText(queryDTO.getSortBy())) {
            Sort.Direction direction = "asc".equalsIgnoreCase(queryDTO.getSortOrder()) 
                ? Sort.Direction.ASC 
                : Sort.Direction.DESC;
            sort = Sort.by(direction, queryDTO.getSortBy());
        }
        
        // 分页查询
        Pageable pageable = PageRequest.of(
            queryDTO.getPage() - 1, 
            queryDTO.getSize(), 
            sort
        );
        
        Page<Product> page = productRepository.findAll(spec, pageable);
        
        // 转换为DTO
        List<ProductResponseDTO> items = page.getContent().stream()
            .map(this::convertToDTO)
            .collect(Collectors.toList());
        
        return new PageResponse<>(
            items,
            page.getTotalElements(),
            queryDTO.getPage(),
            queryDTO.getSize()
        );
    }
    
    @Override
    @Transactional(readOnly = true)
    public ProductResponseDTO findById(Long id) {
        return productRepository.findById(id)
            .map(this::convertToDTO)
            .orElse(null);
    }
    
    @Override
    public ProductResponseDTO create(ProductCreateDTO createDTO) {
        // 验证分类是否存在
        Category category = categoryRepository.findById(createDTO.getCategoryId())
            .orElseThrow(() -> new BusinessException("分类不存在"));
        
        // 创建商品
        Product product = new Product();
        product.setName(createDTO.getName());
        product.setDescription(createDTO.getDescription());
        product.setPrice(createDTO.getPrice());
        product.setStock(createDTO.getStock());
        product.setImageUrl(createDTO.getImageUrl());
        product.setCategory(category);
        product.setStatus(ProductStatus.DRAFT);
        
        product = productRepository.save(product);
        
        return convertToDTO(product);
    }
    
    @Override
    public ProductResponseDTO update(Long id, ProductUpdateDTO updateDTO) {
        Product product = productRepository.findById(id)
            .orElse(null);
        
        if (product == null) {
            return null;
        }
        
        // 更新字段
        if (StringUtils.hasText(updateDTO.getName())) {
            product.setName(updateDTO.getName());
        }
        if (StringUtils.hasText(updateDTO.getDescription())) {
            product.setDescription(updateDTO.getDescription());
        }
        if (updateDTO.getPrice() != null) {
            product.setPrice(updateDTO.getPrice());
        }
        if (updateDTO.getStock() != null) {
            product.setStock(updateDTO.getStock());
        }
        if (StringUtils.hasText(updateDTO.getImageUrl())) {
            product.setImageUrl(updateDTO.getImageUrl());
        }
        if (updateDTO.getCategoryId() != null) {
            Category category = categoryRepository.findById(updateDTO.getCategoryId())
                .orElseThrow(() -> new BusinessException("分类不存在"));
            product.setCategory(category);
        }
        if (updateDTO.getStatus() != null) {
            product.setStatus(updateDTO.getStatus());
        }
        
        product = productRepository.save(product);
        
        return convertToDTO(product);
    }
    
    @Override
    public boolean delete(Long id) {
        if (!productRepository.existsById(id)) {
            return false;
        }
        
        productRepository.deleteById(id);
        return true;
    }
    
    @Override
    @Transactional(readOnly = true)
    public PageResponse<ProductResponseDTO> search(String keyword, int page, int size) {
        ProductQueryDTO queryDTO = new ProductQueryDTO();
        queryDTO.setKeyword(keyword);
        queryDTO.setPage(page);
        queryDTO.setSize(size);
        
        return findProducts(queryDTO);
    }
    
    @Override
    @Transactional(readOnly = true)
    public Integer getStock(Long id) {
        return productRepository.findById(id)
            .map(Product::getStock)
            .orElse(null);
    }
    
    /**
     * 转换为DTO
     */
    private ProductResponseDTO convertToDTO(Product product) {
        ProductResponseDTO dto = new ProductResponseDTO();
        dto.setId(product.getId());
        dto.setName(product.getName());
        dto.setDescription(product.getDescription());
        dto.setPrice(product.getPrice());
        dto.setStock(product.getStock());
        dto.setImageUrl(product.getImageUrl());
        dto.setStatus(product.getStatus());
        dto.setCreatedAt(product.getCreatedAt());
        dto.setUpdatedAt(product.getUpdatedAt());
        
        if (product.getCategory() != null) {
            CategoryDTO categoryDTO = new CategoryDTO();
            categoryDTO.setId(product.getCategory().getId());
            categoryDTO.setName(product.getCategory().getName());
            categoryDTO.setDescription(product.getCategory().getDescription());
            dto.setCategory(categoryDTO);
        }
        
        return dto;
    }
}

ic PageResponse findProducts(ProductQueryDTO queryDTO) {
// 构建查询条件
Specification spec = (root, query, cb) -> {
List predicates = new ArrayList<>();

        // 关键词搜索
        if (StringUtils.hasText(queryDTO.getKeyword())) {
            String keyword = "%" + queryDTO.getKeyword() + "%";
            predicates.add(cb.or(
                cb.like(root.get("name"), keyword),
                cb.like(root.get("description"), keyword)
            ));
        }
        
        // 分类筛选
        if (queryDTO.getCategoryId() != null) {
            predicates.add(cb.equal(
                root.get("category").get("id"), 
                queryDTO.getCategoryId()
            ));
        }
        
        // 价格范围筛选
        if (queryDTO.getMinPrice() != null) {
            predicates.add(cb.greaterThanOrEqualTo(
                root.get("price"), 
                queryDTO.getMinPrice()
            ));
        }
        if (queryDTO.getMaxPrice() != null) {
            predicates.add(cb.lessThanOrEqualTo(
                root.get("price"), 
                queryDTO.getMaxPrice()
            ));
        }
        
        // 状态筛选
        if (queryDTO.getStatus() != null) {
            predicates.add(cb.equal(
                root.get("status"), 
                queryDTO.getStatus()
            ));
        }
        
        return cb.and(predicates.toArray(new Predicate[0]));
    };
    
    // 构建排序
    Sort sort = Sort.unsorted();
    if (StringUtils.hasText(queryDTO.getSortBy())) {
        Sort.Direction direction = "desc".equalsIgnoreCase(
            queryDTO.getSortOrder()) ? 
            Sort.Direction.DESC : Sort.Direction.ASC;
        sort = Sort.by(direction, queryDTO.getSortBy());
    }
    
    // 分页查询
    PageRequest pageRequest = PageRequest.of(
        queryDTO.getPage() - 1, 
        queryDTO.getSize(), 
        sort
    );
    
    Page<Product> page = productRepository.findAll(spec, pageRequest);
    
    // 转换为DTO
    List<ProductResponseDTO> items = page.getContent().stream()
        .map(this::convertToDTO)
        .collect(Collectors.toList());
    
    return new PageResponse<>(
        items,
        page.getTotalElements(),
        queryDTO.getPage(),
        queryDTO.getSize()
    );
}

@Override
@Transactional(readOnly = true)
public ProductResponseDTO findById(Long id) {
    return productRepository.findById(id)
        .map(this::convertToDTO)
        .orElse(null);
}

@Override
public ProductResponseDTO create(ProductCreateDTO createDTO) {
    // 验证分类是否存在
    Category category = categoryRepository.findById(createDTO.getCategoryId())
        .orElseThrow(() -> new BusinessException("分类不存在"));
    
    // 创建商品
    Product product = new Product();
    product.setName(createDTO.getName());
    product.setDescription(createDTO.getDescription());
    product.setPrice(createDTO.getPrice());
    product.setStock(createDTO.getStock());
    product.setImageUrl(createDTO.getImageUrl());
    product.setCategory(category);
    product.setStatus(ProductStatus.DRAFT);
    
    Product savedProduct = productRepository.save(product);
    
    return convertToDTO(savedProduct);
}

@Override
public ProductResponseDTO update(Long id, ProductUpdateDTO updateDTO) {
    Product product = productRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("商品不存在"));
    
    // 更新字段
    if (StringUtils.hasText(updateDTO.getName())) {
        product.setName(updateDTO.getName());
    }
    if (StringUtils.hasText(updateDTO.getDescription())) {
        product.setDescription(updateDTO.getDescription());
    }
    if (updateDTO.getPrice() != null) {
        product.setPrice(updateDTO.getPrice());
    }
    if (updateDTO.getStock() != null) {
        product.setStock(updateDTO.getStock());
    }
    if (StringUtils.hasText(updateDTO.getImageUrl())) {
        product.setImageUrl(updateDTO.getImageUrl());
    }
    if (updateDTO.getCategoryId() != null) {
        Category category = categoryRepository.findById(updateDTO.getCategoryId())
            .orElseThrow(() -> new BusinessException("分类不存在"));
        product.setCategory(category);
    }
    if (updateDTO.getStatus() != null) {
        product.setStatus(updateDTO.getStatus());
    }
    
    Product updatedProduct = productRepository.save(product);
    
    return convertToDTO(updatedProduct);
}

@Override
public boolean delete(Long id) {
    if (!productRepository.existsById(id)) {
        return false;
    }
    
    productRepository.deleteById(id);
    return true;
}

@Override
@Transactional(readOnly = true)
public PageResponse<ProductResponseDTO> search(String keyword, int page, int size) {
    ProductQueryDTO queryDTO = new ProductQueryDTO();
    queryDTO.setKeyword(keyword);
    queryDTO.setPage(page);
    queryDTO.setSize(size);
    
    return findProducts(queryDTO);
}

@Override
@Transactional(readOnly = true)
public Integer getStock(Long id) {
    return productRepository.findById(id)
        .map(Product::getStock)
        .orElse(null);
}

/**
 * 转换为DTO
 */
private ProductResponseDTO convertToDTO(Product product) {
    ProductResponseDTO dto = new ProductResponseDTO();
    dto.setId(product.getId());
    dto.setName(product.getName());
    dto.setDescription(product.getDescription());
    dto.setPrice(product.getPrice());
    dto.setStock(product.getStock());
    dto.setImageUrl(product.getImageUrl());
    dto.setStatus(product.getStatus());
    dto.setCreatedAt(product.getCreatedAt());
    dto.setUpdatedAt(product.getUpdatedAt());
    
    // 转换分类
    if (product.getCategory() != null) {
        CategoryDTO categoryDTO = new CategoryDTO();
        categoryDTO.setId(product.getCategory().getId());
        categoryDTO.setName(product.getCategory().getName());
        categoryDTO.setDescription(product.getCategory().getDescription());
        dto.setCategory(categoryDTO);
    }
    
    return dto;
}

}


## 四、API版本管理

### 4.1 为什么需要API版本管理?

**版本管理的必要性:**

场景:电商系统已上线,有大量用户在使用

需求变更:

  1. 商品价格字段从price改为priceInfo(包含原价、折扣价)
  2. 商品状态从字符串改为枚举
  3. 新增商品规格字段

问题:

  • 直接修改API会导致老版本客户端无法使用
  • 强制用户升级体验不好
  • 需要同时支持新老版本

解决方案:API版本管理


### 4.2 API版本管理策略

**策略1:URL路径版本(推荐)**

```java
/**
 * URL路径版本
 * 优点:清晰明确,易于理解
 * 缺点:URL变长
 */

// V1版本
@RestController
@RequestMapping("/api/v1/products")
public class ProductV1Controller {
    
    @GetMapping("/{id}")
    public ApiResponse<ProductV1DTO> getProduct(@PathVariable Long id) {
        // V1版本的实现
        ProductV1DTO product = productService.findByIdV1(id);
        return ApiResponse.success(product);
    }
}

// V2版本
@RestController
@RequestMapping("/api/v2/products")
public class ProductV2Controller {
    
    @GetMapping("/{id}")
    public ApiResponse<ProductV2DTO> getProduct(@PathVariable Long id) {
        // V2版本的实现
        ProductV2DTO product = productService.findByIdV2(id);
        return ApiResponse.success(product);
    }
}

/**
 * V1版本DTO
 */
@Data
public class ProductV1DTO {
    private Long id;
    private String name;
    private BigDecimal price;  // 单一价格
    private String status;     // 字符串状态
}

/**
 * V2版本DTO
 */
@Data
public class ProductV2DTO {
    private Long id;
    private String name;
    private PriceInfo priceInfo;  // 价格信息对象
    private ProductStatus status;  // 枚举状态
    private List<Specification> specifications;  // 新增规格
}

@Data
public class PriceInfo {
    private BigDecimal originalPrice;  // 原价
    private BigDecimal discountPrice;  // 折扣价
    private BigDecimal finalPrice;     // 最终价格
}

策略2:请求头版本

/**
 * 请求头版本
 * 优点:URL简洁
 * 缺点:不够直观
 */

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    /**
     * V1版本
     */
    @GetMapping(value = "/{id}", headers = "API-Version=1")
    public ApiResponse<ProductV1DTO> getProductV1(@PathVariable Long id) {
        ProductV1DTO product = productService.findByIdV1(id);
        return ApiResponse.success(product);
    }
    
    /**
     * V2版本
     */
    @GetMapping(value = "/{id}", headers = "API-Version=2")
    public ApiResponse<ProductV2DTO> getProductV2(@PathVariable Long id) {
        ProductV2DTO product = productService.findByIdV2(id);
        return ApiResponse.success(product);
    }
    
    /**
     * 默认版本(最新版本)
     */
    @GetMapping("/{id}")
    public ApiResponse<ProductV2DTO> getProduct(@PathVariable Long id) {
        return getProductV2(id);
    }
}

// 客户端请求示例
GET /api/products/1
Headers:
  API-Version: 2

策略3:查询参数版本

/**
 * 查询参数版本
 * 优点:灵活
 * 缺点:容易被忽略
 */

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @GetMapping("/{id}")
    public ApiResponse<?> getProduct(
            @PathVariable Long id,
            @RequestParam(defaultValue = "2") int version) {
        
        if (version == 1) {
            ProductV1DTO product = productService.findByIdV1(id);
            return ApiResponse.success(product);
        } else {
            ProductV2DTO product = productService.findByIdV2(id);
            return ApiResponse.success(product);
        }
    }
}

// 客户端请求示例
GET /api/products/1?version=2

4.3 版本兼容性处理

向后兼容:

/**
 * 版本转换服务
 * 保证新版本兼容老版本
 */
@Service
public class ProductVersionConverter {
    
    /**
     * V2转V1(向后兼容)
     */
    public ProductV1DTO convertV2ToV1(ProductV2DTO v2) {
        ProductV1DTO v1 = new ProductV1DTO();
        v1.setId(v2.getId());
        v1.setName(v2.getName());
        
        // 价格转换:使用最终价格
        v1.setPrice(v2.getPriceInfo().getFinalPrice());
        
        // 状态转换:枚举转字符串
        v1.setStatus(v2.getStatus().name());
        
        return v1;
    }
    
    /**
     * V1转V2(向前兼容)
     */
    public ProductV2DTO convertV1ToV2(ProductV1DTO v1) {
        ProductV2DTO v2 = new ProductV2DTO();
        v2.setId(v1.getId());
        v2.setName(v1.getName());
        
        // 价格转换:单一价格转价格信息
        PriceInfo priceInfo = new PriceInfo();
        priceInfo.setOriginalPrice(v1.getPrice());
        priceInfo.setDiscountPrice(v1.getPrice());
        priceInfo.setFinalPrice(v1.getPrice());
        v2.setPriceInfo(priceInfo);
        
        // 状态转换:字符串转枚举
        v2.setStatus(ProductStatus.valueOf(v1.getStatus()));
        
        // 新字段设置默认值
        v2.setSpecifications(new ArrayList<>());
        
        return v2;
    }
}

4.4 版本废弃策略

/**
 * API废弃注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiDeprecated {
    String since();           // 废弃版本
    String removeIn();        // 移除版本
    String replacement();     // 替代API
    String reason();          // 废弃原因
}

/**
 * 使用示例
 */
@RestController
@RequestMapping("/api/v1/products")
@ApiDeprecated(
    since = "2.0.0",
    removeIn = "3.0.0",
    replacement = "/api/v2/products",
    reason = "V1版本功能有限,请使用V2版本"
)
public class ProductV1Controller {
    
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<ProductV1DTO>> getProduct(
            @PathVariable Long id) {
        
        ProductV1DTO product = productService.findByIdV1(id);
        
        // 添加废弃警告头
        return ResponseEntity.ok()
            .header("Warning", "299 - \"API已废弃,将在3.0.0版本移除,请使用/api/v2/products\"")
            .header("Sunset", "2025-12-31T23:59:59Z")  // 废弃日期
            .body(ApiResponse.success(product));
    }
}

五、API安全设计

5.1 认证与授权

JWT Token认证:

/**
 * JWT工具类
 */
@Component
public class JwtUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private Long expiration;
    
    /**
     * 生成Token
     */
    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getId());
        claims.put("username", user.getUsername());
        claims.put("roles", user.getRoles());
        
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(user.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
    
    /**
     * 验证Token
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    /**
     * 从Token获取用户信息
     */
    public Claims getClaimsFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
    }
}

/**
 * JWT认证过滤器
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain)
            throws ServletException, IOException {
        
        // 1. 从请求头获取Token
        String token = getTokenFromRequest(request);
        
        if (token != null && jwtUtil.validateToken(token)) {
            // 2. 验证Token
            Claims claims = jwtUtil.getClaimsFromToken(token);
            
            // 3. 设置认证信息
            String username = claims.getSubject();
            List<String> roles = (List<String>) claims.get("roles");
            
            List<GrantedAuthority> authorities = roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
            
            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                    username, null, authorities);
            
            SecurityContextHolder.getContext()
                .setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && 
            bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

权限控制:

/**
 * 安全配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()  // 认证接口公开
                .antMatchers("/api/public/**").permitAll()  // 公开接口
                .antMatchers(HttpMethod.GET, "/api/products/**").permitAll()  // 商品查询公开
                .antMatchers("/api/admin/**").hasRole("ADMIN")  // 管理员接口
                .anyRequest().authenticated()  // 其他接口需要认证
            .and()
            .addFilterBefore(jwtAuthenticationFilter, 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

/**
 * 使用方法级权限控制
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    /**
     * 创建商品(需要ADMIN角色)
     */
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<ProductResponseDTO> createProduct(
            @Valid @RequestBody ProductCreateDTO createDTO) {
        ProductResponseDTO product = productService.create(createDTO);
        return ApiResponse.success(product);
    }
    
    /**
     * 更新商品(需要ADMIN或EDITOR角色)
     */
    @PutMapping("/{id}")
    @PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')")
    public ApiResponse<ProductResponseDTO> updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody ProductUpdateDTO updateDTO) {
        ProductResponseDTO product = productService.update(id, updateDTO);
        return ApiResponse.success(product);
    }
    
    /**
     * 删除商品(需要ADMIN角色且是商品所有者)
     */
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') and @productSecurity.isOwner(#id)")
    public ApiResponse<Void> deleteProduct(@PathVariable Long id) {
        productService.delete(id);
        return ApiResponse.success(null);
    }
}

/**
 * 自定义权限检查
 */
@Component("productSecurity")
public class ProductSecurityService {
    
    @Autowired
    private ProductRepository productRepository;
    
    public boolean isOwner(Long productId) {
        Authentication authentication = 
            SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        
        return productRepository.findById(productId)
            .map(product -> product.getCreatedBy().equals(username))
            .orElse(false);
    }
}

5.2 API限流

/**
 * 限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    int limit() default 100;      // 限制次数
    int period() default 60;      // 时间窗口(秒)
    String key() default "";      // 限流key
}

/**
 * 限流切面
 */
@Aspect
@Component
public class RateLimitAspect {
    
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) 
            throws Throwable {
        
        // 1. 获取限流key
        String key = getRateLimitKey(joinPoint, rateLimit);
        
        // 2. 获取当前计数
        Integer count = redisTemplate.opsForValue().get(key);
        
        if (count == null) {
            // 首次请求,设置计数为1
            redisTemplate.opsForValue().set(key, 1, 
                rateLimit.period(), TimeUnit.SECONDS);
        } else if (count < rateLimit.limit()) {
            // 未达到限制,计数+1
            redisTemplate.opsForValue().increment(key);
        } else {
            // 达到限制,抛出异常
            throw new RateLimitException("请求过于频繁,请稍后重试");
        }
        
        return joinPoint.proceed();
    }
    
    private String getRateLimitKey(ProceedingJoinPoint joinPoint, 
                                   RateLimit rateLimit) {
        // 获取用户标识(IP或用户ID)
        HttpServletRequest request = 
            ((ServletRequestAttributes) RequestContextHolder
                .currentRequestAttributes()).getRequest();
        
        String userKey = getUserKey(request);
        String methodKey = joinPoint.getSignature().toShortString();
        
        return "rate_limit:" + methodKey + ":" + userKey;
    }
    
    private String getUserKey(HttpServletRequest request) {
        // 优先使用用户ID
        Authentication authentication = 
            SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            return authentication.getName();
        }
        
        // 否则使用IP地址
        return getClientIP(request);
    }
    
    private String getClientIP(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty()) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

/**
 * 使用限流
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    /**
     * 限制每个用户每分钟最多查询100次
     */
    @GetMapping
    @RateLimit(limit = 100, period = 60)
    public ApiResponse<PageResponse<ProductResponseDTO>> getProducts(
            @Valid ProductQueryDTO queryDTO) {
        PageResponse<ProductResponseDTO> products = 
            productService.findProducts(queryDTO);
        return ApiResponse.success(products);
    }
    
    /**
     * 限制每个用户每分钟最多创建10个商品
     */
    @PostMapping
    @RateLimit(limit = 10, period = 60)
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<ProductResponseDTO> createProduct(
            @Valid @RequestBody ProductCreateDTO createDTO) {
        ProductResponseDTO product = productService.create(createDTO);
        return ApiResponse.success(product);
    }
}

5.3 防重放攻击

/**
 * 防重放注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreventReplay {
    int timeout() default 300;  // 超时时间(秒)
}

/**
 * 防重放切面
 */
@Aspect
@Component
public class PreventReplayAspect {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Around("@annotation(preventReplay)")
    public Object around(ProceedingJoinPoint joinPoint, PreventReplay preventReplay) 
            throws Throwable {
        
        HttpServletRequest request = 
            ((ServletRequestAttributes) RequestContextHolder
                .currentRequestAttributes()).getRequest();
        
        // 1. 获取请求签名
        String signature = request.getHeader("X-Request-Signature");
        String timestamp = request.getHeader("X-Request-Timestamp");
        String nonce = request.getHeader("X-Request-Nonce");
        
        if (signature == null || timestamp == null || nonce == null) {
            throw new SecurityException("缺少安全头");
        }
        
        // 2. 验证时间戳(防止重放旧请求)
        long requestTime = Long.parseLong(timestamp);
        long currentTime = System.currentTimeMillis();
        if (Math.abs(currentTime - requestTime) > preventReplay.timeout() * 1000) {
            throw new SecurityException("请求已过期");
        }
        
        // 3. 验证nonce(防止重复请求)
        String nonceKey = "nonce:" + nonce;
        Boolean exists = redisTemplate.hasKey(nonceKey);
        if (Boolean.TRUE.equals(exists)) {
            throw new SecurityException("请求已被处理");
        }
        
        // 4. 验证签名
        String expectedSignature = generateSignature(request, timestamp, nonce);
        if (!signature.equals(expectedSignature)) {
            throw new SecurityException("签名验证失败");
        }
        
        // 5. 记录nonce
        redisTemplate.opsForValue().set(nonceKey, "1", 
            preventReplay.timeout(), TimeUnit.SECONDS);
        
        return joinPoint.proceed();
    }
    
    private String generateSignature(HttpServletRequest request, 
                                     String timestamp, String nonce) {
        // 生成签名:MD5(method + uri + timestamp + nonce + secret)
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String secret = "your-secret-key";
        
        String data = method + uri + timestamp + nonce + secret;
        
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("生成签名失败", e);
        }
    }
}

/**
 * 使用防重放
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    /**
     * 创建订单(防止重复提交)
     */
    @PostMapping
    @PreventReplay(timeout = 300)
    public ApiResponse<OrderResponseDTO> createOrder(
            @Valid @RequestBody OrderCreateDTO createDTO) {
        OrderResponseDTO order = orderService.create(createDTO);
        return ApiResponse.success(order);
    }
}

5.4 敏感数据加密

/**
 * 敏感数据加密工具
 */
@Component
public class EncryptionUtil {
    
    @Value("${encryption.key}")
    private String encryptionKey;
    
    /**
     * AES加密
     */
    public String encrypt(String data) {
        try {
            SecretKeySpec key = new SecretKeySpec(
                encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            
            byte[] encrypted = cipher.doFinal(
                data.getBytes(StandardCharsets.UTF_8));
            
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("加密失败", e);
        }
    }
    
    /**
     * AES解密
     */
    public String decrypt(String encryptedData) {
        try {
            SecretKeySpec key = new SecretKeySpec(
                encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, key);
            
            byte[] decrypted = cipher.doFinal(
                Base64.getDecoder().decode(encryptedData));
            
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("解密失败", e);
        }
    }
}

/**
 * 敏感字段加密注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypted {
}

/**
 * 用户实体(包含敏感信息)
 */
@Data
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    
    @Encrypted  // 加密字段
    private String email;
    
    @Encrypted  // 加密字段
    private String phone;
    
    private String password;  // 密码使用BCrypt加密
}

/**
 * JPA监听器(自动加密解密)
 */
@Component
public class EncryptionListener {
    
    @Autowired
    private EncryptionUtil encryptionUtil;
    
    @PrePersist
    @PreUpdate
    public void encryptFields(Object entity) {
        Field[] fields = entity.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Encrypted.class)) {
                field.setAccessible(true);
                try {
                    String value = (String) field.get(entity);
                    if (value != null && !value.isEmpty()) {
                        String encrypted = encryptionUtil.encrypt(value);
                        field.set(entity, encrypted);
                    }
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("加密字段失败", e);
                }
            }
        }
    }
    
    @PostLoad
    public void decryptFields(Object entity) {
        Field[] fields = entity.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Encrypted.class)) {
                field.setAccessible(true);
                try {
                    String value = (String) field.get(entity);
                    if (value != null && !value.isEmpty()) {
                        String decrypted = encryptionUtil.decrypt(value);
                        field.set(entity, decrypted);
                    }
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("解密字段失败", e);
                }
            }
        }
    }
}

六、Swagger API文档

6.1 为什么需要API文档?

API文档的重要性:

场景:前后端分离开发

问题:
1. 前端不知道后端有哪些接口
2. 不知道接口的参数和返回值
3. 接口变更后前端不知道
4. 手动编写文档费时费力且容易过时

解决方案:Swagger自动生成API文档
- 自动生成文档
- 实时更新
- 在线测试
- 代码即文档

6.2 集成Swagger

添加依赖:

<!-- pom.xml -->
<dependencies>
    <!-- Swagger3 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>
</dependencies>

Swagger配置:

/**
 * Swagger配置类
 */
@Configuration
@EnableOpenApi
public class SwaggerConfig {
    
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.OAS_30)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.example.shop.controller"))
            .paths(PathSelectors.ant("/api/**"))
            .build()
            .globalRequestParameters(globalRequestParameters())
            .globalResponses(HttpMethod.GET, globalResponses())
            .globalResponses(HttpMethod.POST, globalResponses())
            .globalResponses(HttpMethod.PUT, globalResponses())
            .globalResponses(HttpMethod.DELETE, globalResponses());
    }
    
    /**
     * API信息
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("电商系统API文档")
            .description("电商系统RESTful API接口文档")
            .version("2.0.0")
            .contact(new Contact("张三", "https://www.example.com", "zhangsan@example.com"))
            .license("Apache 2.0")
            .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0")
            .build();
    }
    
    /**
     * 全局请求参数
     */
    private List<RequestParameter> globalRequestParameters() {
        List<RequestParameter> parameters = new ArrayList<>();
        
        parameters.add(new RequestParameterBuilder()
            .name("Authorization")
            .description("认证Token")
            .in(ParameterType.HEADER)
            .required(false)
            .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
            .build());
        
        return parameters;
    }
    
    /**
     * 全局响应
     */
    private List<Response> globalResponses() {
        List<Response> responses = new ArrayList<>();
        
        responses.add(new ResponseBuilder()
            .code("200")
            .description("成功")
            .build());
        
        responses.add(new ResponseBuilder()
            .code("400")
            .description("请求参数错误")
            .build());
        
        responses.add(new ResponseBuilder()
            .code("401")
            .description("未认证")
            .build());
        
        responses.add(new ResponseBuilder()
            .code("403")
            .description("无权限")
            .build());
        
        responses.add(new ResponseBuilder()
            .code("404")
            .description("资源不存在")
            .build());
        
        responses.add(new ResponseBuilder()
            .code("500")
            .description("服务器内部错误")
            .build());
        
        return responses;
    }
}

6.3 Swagger注解使用

Controller注解:

/**
 * 商品控制器
 */
@RestController
@RequestMapping("/api/products")
@Api(tags = "商品管理", description = "商品相关接口")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    /**
     * 查询商品列表
     */
    @GetMapping
    @ApiOperation(value = "查询商品列表", notes = "支持分页、排序、筛选")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "keyword", value = "搜索关键词", paramType = "query", dataType = "String"),
        @ApiImplicitParam(name = "categoryId", value = "分类ID", paramType = "query", dataType = "Long"),
        @ApiImplicitParam(name = "minPrice", value = "最低价格", paramType = "query", dataType = "BigDecimal"),
        @ApiImplicitParam(name = "maxPrice", value = "最高价格", paramType = "query", dataType = "BigDecimal"),
        @ApiImplicitParam(name = "status", value = "商品状态", paramType = "query", dataType = "String"),
        @ApiImplicitParam(name = "sortBy", value = "排序字段", paramType = "query", dataType = "String"),
        @ApiImplicitParam(name = "sortOrder", value = "排序方向(asc/desc)", paramType = "query", dataType = "String"),
        @ApiImplicitParam(name = "page", value = "页码", paramType = "query", dataType = "int", defaultValue = "1"),
        @ApiImplicitParam(name = "size", value = "每页大小", paramType = "query", dataType = "int", defaultValue = "20")
    })
    @ApiResponses({
        @ApiResponse(code = 200, message = "成功", response = PageResponse.class),
        @ApiResponse(code = 400, message = "参数错误")
    })
    public ApiResponse<PageResponse<ProductResponseDTO>> getProducts(
            @Valid ProductQueryDTO queryDTO) {
        PageResponse<ProductResponseDTO> products = 
            productService.findProducts(queryDTO);
        return ApiResponse.success(products);
    }
    
    /**
     * 查询商品详情
     */
    @GetMapping("/{id}")
    @ApiOperation(value = "查询商品详情", notes = "根据商品ID查询详细信息")
    @ApiImplicitParam(name = "id", value = "商品ID", required = true, paramType = "path", dataType = "Long")
    @ApiResponses({
        @ApiResponse(code = 200, message = "成功", response = ProductResponseDTO.class),
        @ApiResponse(code = 404, message = "商品不存在")
    })
    public ApiResponse<ProductResponseDTO> getProduct(
            @PathVariable @ApiParam(value = "商品ID", required = true) Long id) {
        ProductResponseDTO product = productService.findById(id);
        
        if (product == null) {
            return ApiResponse.error(404, "商品不存在");
        }
        
        return ApiResponse.success(product);
    }
    
    /**
     * 创建商品
     */
    @PostMapping
    @ApiOperation(value = "创建商品", notes = "创建新商品(需要管理员权限)")
    @ApiResponses({
        @ApiResponse(code = 201, message = "创建成功", response = ProductResponseDTO.class),
        @ApiResponse(code = 400, message = "参数错误"),
        @ApiResponse(code = 401, message = "未认证"),
        @ApiResponse(code = 403, message = "无权限")
    })
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<ProductResponseDTO> createProduct(
            @Valid @RequestBody @ApiParam(value = "商品信息", required = true) 
            ProductCreateDTO createDTO) {
        ProductResponseDTO product = productService.create(createDTO);
        return ApiResponse.success(product);
    }
    
    /**
     * 更新商品
     */
    @PutMapping("/{id}")
    @ApiOperation(value = "更新商品", notes = "完整更新商品信息(需要管理员权限)")
    @ApiImplicitParam(name = "id", value = "商品ID", required = true, paramType = "path", dataType = "Long")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功", response = ProductResponseDTO.class),
        @ApiResponse(code = 400, message = "参数错误"),
        @ApiResponse(code = 401, message = "未认证"),
        @ApiResponse(code = 403, message = "无权限"),
        @ApiResponse(code = 404, message = "商品不存在")
    })
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<ProductResponseDTO> updateProduct(
            @PathVariable @ApiParam(value = "商品ID", required = true) Long id,
            @Valid @RequestBody @ApiParam(value = "商品信息", required = true) 
            ProductUpdateDTO updateDTO) {
        ProductResponseDTO product = productService.update(id, updateDTO);
        
        if (product == null) {
            return ApiResponse.error(404, "商品不存在");
        }
        
        return ApiResponse.success(product);
    }
    
    /**
     * 删除商品
     */
    @DeleteMapping("/{id}")
    @ApiOperation(value = "删除商品", notes = "删除指定商品(需要管理员权限)")
    @ApiImplicitParam(name = "id", value = "商品ID", required = true, paramType = "path", dataType = "Long")
    @ApiResponses({
        @ApiResponse(code = 200, message = "删除成功"),
        @ApiResponse(code = 401, message = "未认证"),
        @ApiResponse(code = 403, message = "无权限"),
        @ApiResponse(code = 404, message = "商品不存在")
    })
    @PreAuthorize("hasRole('ADMIN')")
    public ApiResponse<Void> deleteProduct(
            @PathVariable @ApiParam(value = "商品ID", required = true) Long id) {
        boolean deleted = productService.delete(id);
        
        if (!deleted) {
            return ApiResponse.error(404, "商品不存在");
        }
        
        return ApiResponse.success(null);
    }
}

Model注解:

/**
 * 商品创建DTO
 */
@Data
@ApiModel(description = "商品创建请求")
public class ProductCreateDTO {
    
    @ApiModelProperty(value = "商品名称", required = true, example = "iPhone 15 Pro")
    @NotBlank(message = "商品名称不能为空")
    @Size(max = 200, message = "商品名称不能超过200个字符")
    private String name;
    
    @ApiModelProperty(value = "商品描述", example = "最新款iPhone,性能强劲")
    @Size(max = 2000, message = "商品描述不能超过2000个字符")
    private String description;
    
    @ApiModelProperty(value = "商品价格", required = true, example = "7999.00")
    @NotNull(message = "商品价格不能为空")
    @DecimalMin(value = "0.01", message = "商品价格必须大于0")
    private BigDecimal price;
    
    @ApiModelProperty(value = "库存数量", required = true, example = "100")
    @NotNull(message = "商品库存不能为空")
    @Min(value = 0, message = "商品库存不能为负数")
    private Integer stock;
    
    @ApiModelProperty(value = "商品图片URL", example = "https://example.com/images/iphone15.jpg")
    private String imageUrl;
    
    @ApiModelProperty(value = "分类ID", required = true, example = "1")
    @NotNull(message = "商品分类不能为空")
    private Long categoryId;
}

/**
 * 商品响应DTO
 */
@Data
@ApiModel(description = "商品响应")
public class ProductResponseDTO {
    
    @ApiModelProperty(value = "商品ID", example = "1")
    private Long id;
    
    @ApiModelProperty(value = "商品名称", example = "iPhone 15 Pro")
    private String name;
    
    @ApiModelProperty(value = "商品描述", example = "最新款iPhone,性能强劲")
    private String description;
    
    @ApiModelProperty(value = "商品价格", example = "7999.00")
    private BigDecimal price;
    
    @ApiModelProperty(value = "库存数量", example = "100")
    private Integer stock;
    
    @ApiModelProperty(value = "商品图片URL", example = "https://example.com/images/iphone15.jpg")
    private String imageUrl;
    
    @ApiModelProperty(value = "商品分类")
    private CategoryDTO category;
    
    @ApiModelProperty(value = "商品状态", example = "PUBLISHED")
    private ProductStatus status;
    
    @ApiModelProperty(value = "创建时间", example = "2024-01-06T10:00:00")
    private LocalDateTime createdAt;
    
    @ApiModelProperty(value = "更新时间", example = "2024-01-06T10:00:00")
    private LocalDateTime updatedAt;
}

6.4 访问Swagger文档

启动应用后,访问:
http://localhost:8080/swagger-ui/index.html

功能:
1. 查看所有API接口
2. 查看接口详细信息(参数、返回值、状态码)
3. 在线测试接口
4. 导出API文档(JSON/YAML格式)

七、API测试

7.1 使用Postman测试

创建Postman Collection:

{
  "info": {
    "name": "电商系统API",
    "description": "电商系统RESTful API测试集合",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "商品管理",
      "item": [
        {
          "name": "查询商品列表",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/products?page=1&size=20",
              "host": ["{{baseUrl}}"],
              "path": ["api", "products"],
              "query": [
                {"key": "page", "value": "1"},
                {"key": "size", "value": "20"}
              ]
            }
          }
        },
        {
          "name": "查询商品详情",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/products/{{productId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "products", "{{productId}}"]
            }
          }
        },
        {
          "name": "创建商品",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{token}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"iPhone 15 Pro\",\n  \"description\": \"最新款iPhone\",\n  \"price\": 7999.00,\n  \"stock\": 100,\n  \"categoryId\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api/products",
              "host": ["{{baseUrl}}"],
              "path": ["api", "products"]
            }
          }
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:8080"
    },
    {
      "key": "token",
      "value": ""
    },
    {
      "key": "productId",
      "value": "1"
    }
  ]
}

Postman测试脚本:

// 测试脚本:验证响应
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("Response has correct structure", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData).to.have.property('code');
    pm.expect(jsonData).to.have.property('message');
    pm.expect(jsonData).to.have.property('data');
});

pm.test("Response code is 200", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData.code).to.eql(200);
});

// 保存响应数据到环境变量
var jsonData = pm.response.json();
if (jsonData.data && jsonData.data.id) {
    pm.environment.set("productId", jsonData.data.id);
}

7.2 单元测试

Controller单元测试:

/**
 * 商品Controller单元测试
 */
@WebMvcTest(ProductController.class)
@AutoConfigureMockMvc(addFilters = false)  // 禁用安全过滤器
public class ProductControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private ProductService productService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    /**
     * 测试查询商品列表
     */
    @Test
    public void testGetProducts() throws Exception {
        // 准备测试数据
        List<ProductResponseDTO> products = Arrays.asList(
            createProductDTO(1L, "iPhone 15 Pro", new BigDecimal("7999.00")),
            createProductDTO(2L, "MacBook Pro", new BigDecimal("12999.00"))
        );
        
        PageResponse<ProductResponseDTO> pageResponse = new PageResponse<>(
            products, 2, 1, 20
        );
        
        // Mock服务方法
        when(productService.findProducts(any(ProductQueryDTO.class)))
            .thenReturn(pageResponse);
        
        // 执行请求
        mockMvc.perform(get("/api/products")
                .param("page", "1")
                .param("size", "20")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.items").isArray())
            .andExpect(jsonPath("$.data.items.length()").value(2))
            .andExpect(jsonPath("$.data.total").value(2))
            .andDo(print());
        
        // 验证服务方法被调用
        verify(productService, times(1)).findProducts(any(ProductQueryDTO.class));
    }
    
    /**
     * 测试查询商品详情
     */
    @Test
    public void testGetProduct() throws Exception {
        // 准备测试数据
        ProductResponseDTO product = createProductDTO(
            1L, "iPhone 15 Pro", new BigDecimal("7999.00")
        );
        
        // Mock服务方法
        when(productService.findById(1L)).thenReturn(product);
        
        // 执行请求
        mockMvc.perform(get("/api/products/1")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.id").value(1))
            .andExpect(jsonPath("$.data.name").value("iPhone 15 Pro"))
            .andExpect(jsonPath("$.data.price").value(7999.00))
            .andDo(print());
        
        verify(productService, times(1)).findById(1L);
    }
    
    /**
     * 测试查询不存在的商品
     */
    @Test
    public void testGetProductNotFound() throws Exception {
        // Mock服务方法返回null
        when(productService.findById(999L)).thenReturn(null);
        
        // 执行请求
        mockMvc.perform(get("/api/products/999")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(404))
            .andExpect(jsonPath("$.message").value("商品不存在"))
            .andDo(print());
        
        verify(productService, times(1)).findById(999L);
    }
    
    /**
     * 测试创建商品
     */
    @Test
    @WithMockUser(roles = "ADMIN")  // 模拟管理员用户
    public void testCreateProduct() throws Exception {
        // 准备测试数据
        ProductCreateDTO createDTO = new ProductCreateDTO();
        createDTO.setName("iPhone 15 Pro");
        createDTO.setDescription("最新款iPhone");
        createDTO.setPrice(new BigDecimal("7999.00"));
        createDTO.setStock(100);
        createDTO.setCategoryId(1L);
        
        ProductResponseDTO responseDTO = createProductDTO(
            1L, "iPhone 15 Pro", new BigDecimal("7999.00")
        );
        
        // Mock服务方法
        when(productService.create(any(ProductCreateDTO.class)))
            .thenReturn(responseDTO);
        
        // 执行请求
        mockMvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDTO)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.id").value(1))
            .andExpect(jsonPath("$.data.name").value("iPhone 15 Pro"))
            .andDo(print());
        
        verify(productService, times(1)).create(any(ProductCreateDTO.class));
    }
    
    /**
     * 测试创建商品(参数校验失败)
     */
    @Test
    @WithMockUser(roles = "ADMIN")
    public void testCreateProductValidationFailed() throws Exception {
        // 准备无效的测试数据
        ProductCreateDTO createDTO = new ProductCreateDTO();
        createDTO.setName("");  // 名称为空
        createDTO.setPrice(new BigDecimal("-100"));  // 价格为负数
        
        // 执行请求
        mockMvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDTO)))
            .andExpect(status().isBadRequest())
            .andDo(print());
    }
    
    /**
     * 测试更新商品
     */
    @Test
    @WithMockUser(roles = "ADMIN")
    public void testUpdateProduct() throws Exception {
        // 准备测试数据
        ProductUpdateDTO updateDTO = new ProductUpdateDTO();
        updateDTO.setName("iPhone 15 Pro Max");
        updateDTO.setPrice(new BigDecimal("8999.00"));
        
        ProductResponseDTO responseDTO = createProductDTO(
            1L, "iPhone 15 Pro Max", new BigDecimal("8999.00")
        );
        
        // Mock服务方法
        when(productService.update(eq(1L), any(ProductUpdateDTO.class)))
            .thenReturn(responseDTO);
        
        // 执行请求
        mockMvc.perform(put("/api/products/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateDTO)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.name").value("iPhone 15 Pro Max"))
            .andExpect(jsonPath("$.data.price").value(8999.00))
            .andDo(print());
        
        verify(productService, times(1)).update(eq(1L), any(ProductUpdateDTO.class));
    }
    
    /**
     * 测试删除商品
     */
    @Test
    @WithMockUser(roles = "ADMIN")
    public void testDeleteProduct() throws Exception {
        // Mock服务方法
        when(productService.delete(1L)).thenReturn(true);
        
        // 执行请求
        mockMvc.perform(delete("/api/products/1")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andDo(print());
        
        verify(productService, times(1)).delete(1L);
    }
    
    /**
     * 创建测试用的ProductDTO
     */
    private ProductResponseDTO createProductDTO(Long id, String name, BigDecimal price) {
        ProductResponseDTO dto = new ProductResponseDTO();
        dto.setId(id);
        dto.setName(name);
        dto.setDescription("测试商品");
        dto.setPrice(price);
        dto.setStock(100);
        dto.setStatus(ProductStatus.PUBLISHED);
        dto.setCreatedAt(LocalDateTime.now());
        dto.setUpdatedAt(LocalDateTime.now());
        return dto;
    }
}

7.3 集成测试

/**
 * 商品API集成测试
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@Transactional
public class ProductIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private CategoryRepository categoryRepository;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    private Category category;
    
    @BeforeEach
    public void setup() {
        // 清理数据
        productRepository.deleteAll();
        categoryRepository.deleteAll();
        
        // 创建测试分类
        category = new Category();
        category.setName("电子产品");
        category.setDescription("电子产品分类");
        category = categoryRepository.save(category);
    }
    
    /**
     * 测试完整的CRUD流程
     */
    @Test
    @WithMockUser(roles = "ADMIN")
    public void testProductCRUD() throws Exception {
        // 1. 创建商品
        ProductCreateDTO createDTO = new ProductCreateDTO();
        createDTO.setName("iPhone 15 Pro");
        createDTO.setDescription("最新款iPhone");
        createDTO.setPrice(new BigDecimal("7999.00"));
        createDTO.setStock(100);
        createDTO.setCategoryId(category.getId());
        
        MvcResult createResult = mockMvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDTO)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.name").value("iPhone 15 Pro"))
            .andReturn();
        
        String createResponse = createResult.getResponse().getContentAsString();
        Long productId = JsonPath.read(createResponse, "$.data.id");
        
        // 2. 查询商品
        mockMvc.perform(get("/api/products/" + productId)
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.id").value(productId))
            .andExpect(jsonPath("$.data.name").value("iPhone 15 Pro"));
        
        // 3. 更新商品
        ProductUpdateDTO updateDTO = new ProductUpdateDTO();
        updateDTO.setName("iPhone 15 Pro Max");
        updateDTO.setPrice(new BigDecimal("8999.00"));
        
        mockMvc.perform(put("/api/products/" + productId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateDTO)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.name").value("iPhone 15 Pro Max"))
            .andExpect(jsonPath("$.data.price").value(8999.00));
        
        // 4. 查询商品列表
        mockMvc.perform(get("/api/products")
                .param("page", "1")
                .param("size", "20")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200))
            .andExpect(jsonPath("$.data.items").isArray())
            .andExpect(jsonPath("$.data.total").value(1));
        
        // 5. 删除商品
        mockMvc.perform(delete("/api/products/" + productId)
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(200));
        
        // 6. 验证商品已删除
        mockMvc.perform(get("/api/products/" + productId)
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value(404));
    }
}

八、面试题精选

8.1 HTTP协议相关面试题

面试题1:HTTP和HTTPS的区别是什么?

答案:

1. 安全性:
   - HTTP:明文传输,不安全
   - HTTPS:加密传输,安全

2. 端口:
   - HTTP:默认端口80
   - HTTPS:默认端口443

3. 证书:
   - HTTP:不需要证书
   - HTTPS:需要SSL/TLS证书

4. 性能:
   - HTTP:速度快
   - HTTPS:稍慢(加密解密开销)

5. SEO:
   - HTTP:普通
   - HTTPS:搜索引擎优先

6. 连接过程:
   - HTTP:TCP三次握手
   - HTTPS:TCP三次握手 + TLS握手

应用场景:
- HTTP:公开信息展示
- HTTPS:登录、支付、敏感信息传输

面试题2:HTTP请求方法有哪些?各有什么特点?

答案:

1. GET:
   - 获取资源
   - 参数在URL中
   - 幂等、安全
   - 可缓存

2. POST:
   - 创建资源
   - 参数在请求体中
   - 非幂等、不安全
   - 不可缓存

3. PUT:
   - 完整更新资源
   - 参数在请求体中
   - 幂等、不安全
   - 不可缓存

4. PATCH:
   - 部分更新资源
   - 参数在请求体中
   - 幂等、不安全
   - 不可缓存

5. DELETE:
   - 删除资源
   - 幂等、不安全
   - 不可缓存

6. HEAD:
   - 获取资源元信息
   - 幂等、安全
   - 可缓存

7. OPTIONS:
   - 获取支持的方法
   - 幂等、安全
   - 用于CORS预检请求

面试题3:HTTP状态码有哪些?分别代表什么?

答案:

1xx - 信息性状态码:
- 100 Continue:继续请求
- 101 Switching Protocols:切换协议

2xx - 成功状态码:
- 200 OK:请求成功
- 201 Created:资源已创建
- 204 No Content:成功但无内容

3xx - 重定向状态码:
- 301 Moved Permanently:永久重定向
- 302 Found:临时重定向
- 304 Not Modified:资源未修改

4xx - 客户端错误:
- 400 Bad Request:请求参数错误
- 401 Unauthorized:未认证
- 403 Forbidden:无权限
- 404 Not Found:资源不存在
- 405 Method Not Allowed:方法不允许
- 409 Conflict:资源冲突
- 422 Unprocessable Entity:语义错误
- 429 Too Many Requests:请求过多

5xx - 服务器错误:
- 500 Internal Server Error:服务器内部错误
- 502 Bad Gateway:网关错误
- 503 Service Unavailable:服务不可用
- 504 Gateway Timeout:网关超时

面试题4:什么是幂等性?哪些HTTP方法是幂等的?

答案:

幂等性定义:
多次执行相同操作,结果相同

幂等的HTTP方法:
1. GET:多次查询,结果相同
2. PUT:多次更新为相同值,结果相同
3. DELETE:多次删除,结果都是不存在
4. HEAD:多次获取元信息,结果相同
5. OPTIONS:多次查询支持的方法,结果相同

非幂等的HTTP方法:
1. POST:多次创建,会产生多个资源

为什么幂等性重要?
1. 网络重试:网络不稳定时可以安全重试
2. 分布式系统:避免重复操作
3. 用户体验:防止重复提交

实现幂等性的方法:
1. 唯一ID:使用唯一标识防止重复
2. Token机制:一次性Token
3. 状态机:检查资源状态
4. 数据库约束:唯一索引

面试题5:HTTP/1.0、HTTP/1.1、HTTP/2的区别?

答案:

HTTP/1.0:
- 短连接:每次请求都要建立连接
- 无Host头:不支持虚拟主机
- 不支持断点续传

HTTP/1.1:
- 持久连接:Connection: keep-alive
- 管道化:可以同时发送多个请求
- Host头:支持虚拟主机
- 断点续传:Range头
- 缓存控制:Cache-Control
- 问题:队头阻塞

HTTP/2:
- 二进制分帧:更高效
- 多路复用:一个连接处理多个请求
- 头部压缩:HPACK算法
- 服务器推送:主动推送资源
- 优先级:请求优先级控制
- 解决队头阻塞问题

HTTP/3:
- 基于QUIC协议
- 基于UDP
- 更快的连接建立
- 更好的拥塞控制

8.2 RESTful API相关面试题

面试题6:什么是RESTful API?它的核心原则是什么?

答案:

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

核心原则:

1. 资源(Resource):
   - 一切皆资源
   - 每个资源有唯一URI
   - 使用名词而非动词

2. 统一接口(Uniform Interface):
   - 使用标准HTTP方法
   - GET、POST、PUT、PATCH、DELETE

3. 无状态(Stateless):
   - 服务器不保存客户端状态
   - 每个请求包含所有必要信息

4. 可缓存(Cacheable):
   - 响应应明确标识是否可缓存
   - 提高性能

5. 分层系统(Layered System):
   - 客户端不知道是否直接连接到服务器
   - 支持负载均衡、缓存等

6. 按需代码(Code on Demand):
   - 可选原则
   - 服务器可以返回可执行代码

优势:
- 简单易懂
- 易于扩展
- 松耦合
- 可缓存
- 跨平台

面试题7:RESTful API的URL应该如何设计?

答案:

URL设计规范:

1. 使用名词,不使用动词:
   ✅ GET /users
   ❌ GET /getUsers

2. 使用复数形式:
   ✅ GET /products
   ❌ GET /product

3. 使用小写字母和连字符:
   ✅ GET /product-categories
   ❌ GET /ProductCategories

4. 表示层级关系:
   ✅ GET /users/123/orders
   ✅ GET /orders/456/items

5. 使用查询参数过滤:
   ✅ GET /products?category=electronics&page=1
   ❌ GET /products/electronics/page/1

6. 版本管理:
   ✅ GET /api/v1/products
   ✅ GET /api/v2/products

7. 避免过深的层级:
   ✅ GET /users/123/orders
   ❌ GET /companies/1/departments/2/teams/3/users/4

示例:
GET    /api/v1/products          # 获取商品列表
GET    /api/v1/products/123      # 获取单个商品
POST   /api/v1/products          # 创建商品
PUT    /api/v1/products/123      # 更新商品
DELETE /api/v1/products/123      # 删除商品
GET    /api/v1/products/123/reviews  # 获取商品评论

面试题8:如何设计一个统一的API响应格式?

答案:

/**
 * 统一响应格式设计
 */
@Data
public class ApiResponse<T> {
    /**
     * 业务状态码
     * 200: 成功
     * 400: 参数错误
     * 401: 未认证
     * 403: 无权限
     * 404: 资源不存在
     * 500: 服务器错误
     */
    private int code;
    
    /**
     * 提示信息
     */
    private String message;
    
    /**
     * 数据
     */
    private T data;
    
    /**
     * 时间戳
     */
    private Long timestamp;
    
    /**
     * 请求ID(用于追踪)
     */
    private String requestId;
    
    /**
     * 成功响应
     */
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("成功");
        response.setData(data);
        response.setTimestamp(System.currentTimeMillis());
        response.setRequestId(UUID.randomUUID().toString());
        return response;
    }
    
    /**
     * 失败响应
     */
    public static <T> ApiResponse<T> error(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMessage(message);
        response.setTimestamp(System.currentTimeMillis());
        response.setRequestId(UUID.randomUUID().toString());
        return response;
    }
}

/**
 * 分页响应格式
 */
@Data
public class PageResponse<T> {
    private List<T> items;      // 数据列表
    private long total;         // 总数
    private int page;           // 当前页
    private int size;           // 每页大小
    private int totalPages;     // 总页数
    private boolean hasNext;    // 是否有下一页
    private boolean hasPrev;    // 是否有上一页
}

设计原则:
1. 结构统一:所有接口使用相同的响应格式
2. 信息完整:包含状态码、消息、数据、时间戳
3. 易于扩展:可以添加新字段
4. 便于调试:包含请求ID用于追踪
5. 类型安全:使用泛型

面试题9:如何实现API版本管理?

答案:

API版本管理策略:

1. URL路径版本(推荐):
   优点:清晰明确,易于理解
   缺点:URL变长
   
   示例:
   GET /api/v1/products
   GET /api/v2/products

2. 请求头版本:
   优点:URL简洁
   缺点:不够直观
   
   示例:
   GET /api/products
   Headers: API-Version: 2

3. 查询参数版本:
   优点:灵活
   缺点:容易被忽略
   
   示例:
   GET /api/products?version=2

4. 媒体类型版本:
   优点:符合REST规范
   缺点:复杂
   
   示例:
   GET /api/products
   Accept: application/vnd.company.v2+json

版本兼容性处理:
1. 向后兼容:新版本兼容老版本
2. 版本转换:提供版本转换服务
3. 废弃策略:提前通知,逐步废弃
4. 文档维护:每个版本独立文档

最佳实践:
1. 使用URL路径版本
2. 主版本号变更表示不兼容
3. 次版本号变更表示向后兼容
4. 至少支持2个版本
5. 提前6个月通知废弃

面试题10:如何保证API的安全性?

答案:

API安全措施:

1. 认证(Authentication):
   - JWT Token:无状态认证
   - OAuth2.0:第三方授权
   - API Key:简单认证
   
   示例:
   Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

2. 授权(Authorization):
   - RBAC:基于角色的访问控制
   - ABAC:基于属性的访问控制
   - 权限注解:@PreAuthorize("hasRole('ADMIN')")

3. HTTPS加密:
   - 强制使用HTTPS
   - TLS 1.2+
   - 配置HSTS头

4. 限流(Rate Limiting):
   - 防止恶意攻击
   - 保护服务器资源
   - 使用Redis实现

5. 防重放攻击:
   - 时间戳验证
   - Nonce机制
   - 请求签名

6. 输入验证:
   - 参数校验:@Valid
   - SQL注入防护:使用PreparedStatement
   - XSS防护:转义特殊字符

7. 敏感数据加密:
   - 密码:BCrypt加密
   - 敏感字段:AES加密
   - 传输加密:HTTPS

8. 安全响应头:
   - X-Frame-Options:防止点击劫持
   - X-Content-Type-Options:防止MIME嗅探
   - X-XSS-Protection:XSS过滤
   - Content-Security-Policy:内容安全策略

9. 日志审计:
   - 记录所有API调用
   - 记录敏感操作
   - 异常告警

10. CORS配置:
    - 限制允许的域名
    - 限制允许的方法
    - 限制允许的头

面试题11:如何设计一个高性能的API?

答案:

API性能优化策略:

1. 缓存:
   - HTTP缓存:Cache-Control、ETag
   - 服务端缓存:Redis
   - CDN缓存:静态资源
   
   示例:
   Cache-Control: public, max-age=3600
   ETag: "686897696a7c876b7e"

2. 分页:
   - 限制返回数据量
   - 使用游标分页(大数据量)
   - 默认分页大小:20

3. 字段过滤:
   - 只返回需要的字段
   - 使用fields参数
   
   示例:
   GET /api/products?fields=id,name,price

4. 异步处理:
   - 耗时操作异步化
   - 返回任务ID
   - 轮询或WebSocket获取结果

5. 批量操作:
   - 支持批量查询
   - 支持批量创建/更新
   
   示例:
   POST /api/products/batch
   GET /api/products?ids=1,2,3

6. 数据库优化:
   - 索引优化
   - 查询优化
   - 连接池配置

7. 压缩:
   - Gzip压缩
   - 减少传输数据量
   
   示例:
   Accept-Encoding: gzip
   Content-Encoding: gzip

8. 限流:
   - 保护服务器资源
   - 防止恶意攻击
   - 令牌桶算法

9. 负载均衡:
   - Nginx负载均衡
   - 多实例部署
   - 健康检查

10. 监控:
    - 响应时间监控
    - 错误率监控
    - 性能分析

面试题12:在项目中如何处理API的错误?

答案:

/**
 * 全局异常处理
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger log = LoggerFactory.getLogger(
        GlobalExceptionHandler.class);
    
    /**
     * 参数校验异常(400)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.toList());
        
        log.warn("参数校验失败: {}", errors);
        
        return ApiResponse.error(400, "参数校验失败: " + 
            String.join(", ", errors));
    }
    
    /**
     * 认证异常(401)
     */
    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ApiResponse<Void> handleAuthenticationException(
            AuthenticationException ex) {
        log.warn("认证失败: {}", ex.getMessage());
        return ApiResponse.error(401, "请先登录");
    }
    
    /**
     * 权限异常(403)
     */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ApiResponse<Void> handleAccessDeniedException(
            AccessDeniedException ex) {
        log.warn("权限不足: {}", ex.getMessage());
        return ApiResponse.error(403, "您没有权限访问该资源");
    }
    
    /**
     * 资源不存在异常(404)
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<Void> handleResourceNotFoundException(
            ResourceNotFoundException ex) {
        log.warn("资源不存在: {}", ex.getMessage());
        return ApiResponse.error(404, ex.getMessage());
    }
    
    /**
     * 业务异常(422)
     */
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public ApiResponse<Void> handleBusinessException(
            BusinessException ex) {
        log.warn("业务异常: {}", ex.getMessage());
        return ApiResponse.error(422, ex.getMessage());
    }
    
    /**
     * 服务器异常(500)
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleException(Exception ex) {
        log.error("服务器内部错误", ex);
        return ApiResponse.error(500, "服务器内部错误,请稍后重试");
    }
}

错误处理原则:
1. 统一处理:使用全局异常处理器
2. 分类处理:不同异常返回不同状态码
3. 信息安全:不暴露内部实现细节
4. 日志记录:记录所有异常
5. 用户友好:返回易懂的错误信息

九、练习题

9.1 基础练习

练习1:实现用户管理API

要求:

  1. 实现用户的CRUD操作
  2. 使用RESTful风格设计URL
  3. 实现参数校验
  4. 实现统一响应格式
  5. 实现全局异常处理
/**
 * 用户实体
 */
@Data
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    @Column(nullable = false, length = 100)
    private String password;
    
    @Column(nullable = false, unique = true, length = 100)
    private String email;
    
    @Column(length = 20)
    private String phone;
    
    @Column(nullable = false)
    private UserStatus status;
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

/**
 * 任务:
 * 1. 创建UserController,实现以下接口:
 *    - GET /api/users - 查询用户列表(支持分页)
 *    - GET /api/users/{id} - 查询用户详情
 *    - POST /api/users - 创建用户
 *    - PUT /api/users/{id} - 更新用户
 *    - DELETE /api/users/{id} - 删除用户
 * 
 * 2. 创建UserService,实现业务逻辑
 * 
 * 3. 创建UserDTO,实现数据传输对象
 * 
 * 4. 实现参数校验(用户名、邮箱格式等)
 * 
 * 5. 实现全局异常处理
 */

练习2:实现订单管理API

要求:

  1. 实现订单的创建、查询、取消
  2. 实现订单状态流转
  3. 实现订单与用户的关联
  4. 实现订单与商品的关联
  5. 实现订单统计接口
/**
 * 订单实体
 */
@Data
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 32)
    private String orderNo;
    
    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;
    
    @Column(nullable = false)
    private OrderStatus status;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> items;
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

/**
 * 任务:
 * 1. 设计订单相关的RESTful API
 * 2. 实现订单创建(包含订单项)
 * 3. 实现订单状态流转(待支付->已支付->已发货->已完成)
 * 4. 实现订单查询(支持按状态、时间范围筛选)
 * 5. 实现订单取消(只能取消待支付订单)
 * 6. 实现订单统计(总金额、订单数量等)
 */

9.2 进阶练习

练习3:实现API版本管理

要求:

  1. 实现V1和V2两个版本的商品API
  2. V1版本:简单的商品信息
  3. V2版本:增强的商品信息(包含规格、评分等)
  4. 实现版本转换服务
  5. 实现版本废弃警告
/**
 * 任务:
 * 1. 创建ProductV1Controller和ProductV2Controller
 * 2. V1版本返回基本商品信息
 * 3. V2版本返回增强商品信息
 * 4. 实现版本转换服务(V1<->V2)
 * 5. 为V1版本添加废弃警告头
 * 6. 编写测试用例验证两个版本
 */

练习4:实现API安全机制

要求:

  1. 实现JWT认证
  2. 实现基于角色的权限控制
  3. 实现API限流
  4. 实现防重放攻击
  5. 实现敏感数据加密
/**
 * 任务:
 * 1. 实现JWT工具类(生成、验证Token)
 * 2. 实现JWT认证过滤器
 * 3. 实现权限注解和权限检查
 * 4. 实现限流注解和限流切面
 * 5. 实现防重放注解和防重放切面
 * 6. 实现敏感字段加密(邮箱、手机号)
 * 7. 编写测试用例验证安全机制
 */

练习5:实现API文档和测试

要求:

  1. 集成Swagger生成API文档
  2. 添加详细的API注解
  3. 编写Controller单元测试
  4. 编写集成测试
  5. 创建Postman测试集合
/**
 * 任务:
 * 1. 集成Swagger 3.0
 * 2. 为所有Controller添加Swagger注解
 * 3. 为所有DTO添加ApiModel注解
 * 4. 编写Controller单元测试(使用MockMvc)
 * 5. 编写集成测试(测试完整流程)
 * 6. 创建Postman Collection
 * 7. 编写Postman测试脚本
 */

9.3 综合练习

练习6:实现完整的电商API系统

要求:

  1. 用户管理:注册、登录、个人信息管理
  2. 商品管理:商品CRUD、分类管理、库存管理
  3. 购物车:添加商品、修改数量、删除商品
  4. 订单管理:创建订单、支付订单、查询订单
  5. 评论管理:发表评论、查询评论、删除评论
/**
 * 系统架构:
 * 
 * 1. 用户模块:
 *    - POST /api/auth/register - 用户注册
 *    - POST /api/auth/login - 用户登录
 *    - GET /api/users/profile - 获取个人信息
 *    - PUT /api/users/profile - 更新个人信息
 * 
 * 2. 商品模块:
 *    - GET /api/products - 查询商品列表
 *    - GET /api/products/{id} - 查询商品详情
 *    - POST /api/products - 创建商品(管理员)
 *    - PUT /api/products/{id} - 更新商品(管理员)
 *    - DELETE /api/products/{id} - 删除商品(管理员)
 *    - GET /api/categories - 查询分类列表
 * 
 * 3. 购物车模块:
 *    - GET /api/cart - 查询购物车
 *    - POST /api/cart/items - 添加商品到购物车
 *    - PUT /api/cart/items/{id} - 修改购物车商品数量
 *    - DELETE /api/cart/items/{id} - 删除购物车商品
 *    - DELETE /api/cart - 清空购物车
 * 
 * 4. 订单模块:
 *    - POST /api/orders - 创建订单
 *    - GET /api/orders - 查询订单列表
 *    - GET /api/orders/{id} - 查询订单详情
 *    - PUT /api/orders/{id}/pay - 支付订单
 *    - PUT /api/orders/{id}/cancel - 取消订单
 * 
 * 5. 评论模块:
 *    - GET /api/products/{id}/reviews - 查询商品评论
 *    - POST /api/reviews - 发表评论
 *    - DELETE /api/reviews/{id} - 删除评论
 * 
 * 技术要求:
 * 1. 使用Spring Boot 2.5+
 * 2. 使用Spring Data JPA
 * 3. 使用MySQL数据库
 * 4. 使用Redis缓存
 * 5. 使用JWT认证
 * 6. 使用Swagger文档
 * 7. 编写单元测试和集成测试
 * 8. 实现全局异常处理
 * 9. 实现统一响应格式
 * 10. 实现API限流
 */

十、学习检查清单

10.1 HTTP协议基础

  • 理解HTTP协议的工作原理
  • 掌握HTTP请求和响应的结构
  • 理解HTTP的无状态特性
  • 掌握HTTP持久连接
  • 理解HTTP/1.0、HTTP/1.1、HTTP/2的区别

10.2 HTTP请求方法

  • 掌握GET、POST、PUT、PATCH、DELETE的使用
  • 理解幂等性的概念
  • 理解安全性的概念
  • 能够根据场景选择合适的HTTP方法
  • 理解HEAD、OPTIONS等其他方法

10.3 HTTP状态码

  • 掌握常用的2xx成功状态码
  • 掌握常用的3xx重定向状态码
  • 掌握常用的4xx客户端错误状态码
  • 掌握常用的5xx服务器错误状态码
  • 能够根据场景返回合适的状态码

10.4 HTTP请求头和响应头

  • 掌握Content-Type的使用
  • 掌握Authorization的使用
  • 掌握Cache-Control的使用
  • 掌握Cookie和Set-Cookie的使用
  • 理解CORS相关的头

10.5 HTTPS加密

  • 理解HTTPS的工作原理
  • 理解对称加密和非对称加密
  • 理解SSL/TLS握手过程
  • 能够配置Spring Boot使用HTTPS
  • 理解HTTPS的性能影响

10.6 RESTful设计原则

  • 理解REST的核心概念
  • 掌握资源导向的设计思想
  • 掌握统一接口的设计原则
  • 理解无状态的设计原则
  • 理解可缓存的设计原则

10.7 RESTful URL设计

  • 掌握URL命名规范
  • 能够设计清晰的资源层级
  • 掌握查询参数的使用
  • 能够设计合理的分页接口
  • 能够设计合理的搜索接口

10.8 RESTful响应设计

  • 能够设计统一的响应格式
  • 能够设计合理的分页响应
  • 能够设计合理的错误响应
  • 理解响应数据的优化方法
  • 掌握字段过滤的实现

10.9 API版本管理

  • 理解API版本管理的必要性
  • 掌握URL路径版本管理
  • 掌握请求头版本管理
  • 能够实现版本兼容性处理
  • 能够实现版本废弃策略

10.10 API安全

  • 掌握JWT认证的实现
  • 掌握基于角色的权限控制
  • 掌握API限流的实现
  • 掌握防重放攻击的实现
  • 掌握敏感数据加密的实现

10.11 API文档

  • 能够集成Swagger生成文档
  • 掌握Swagger注解的使用
  • 能够编写清晰的API文档
  • 能够使用Swagger UI测试接口
  • 能够导出API文档

10.12 API测试

  • 能够使用Postman测试API
  • 能够编写Controller单元测试
  • 能够编写集成测试
  • 能够编写测试脚本
  • 能够进行性能测试

10.13 项目实战

  • 能够设计完整的RESTful API
  • 能够实现用户认证和授权
  • 能够实现全局异常处理
  • 能够实现统一响应格式
  • 能够实现API监控和日志

十一、知识总结

11.1 HTTP协议核心要点

HTTP协议

请求响应模型

请求行

请求头

请求体

响应行

响应头

响应体

请求方法

GET查询

POST创建

PUT更新

PATCH部分更新

DELETE删除

状态码

2xx成功

3xx重定向

4xx客户端错误

5xx服务器错误

特性

无状态

持久连接

可缓存

分层系统

11.2 RESTful API设计要点

1. 资源导向:
   - 使用名词而非动词
   - 使用复数形式
   - 清晰的层级关系

2. 统一接口:
   - 使用标准HTTP方法
   - 返回合适的状态码
   - 统一的响应格式

3. 无状态:
   - 不依赖服务器session
   - 使用Token传递身份信息
   - 每个请求包含所有必要信息

4. 可缓存:
   - 使用Cache-Control
   - 使用ETag
   - 使用Last-Modified

5. 安全性:
   - HTTPS加密
   - JWT认证
   - 权限控制
   - 限流保护
   - 防重放攻击

11.3 API开发最佳实践

1. 设计阶段:
   - 遵循RESTful规范
   - 设计清晰的URL
   - 设计统一的响应格式
   - 考虑版本管理
   - 考虑安全性

2. 开发阶段:
   - 使用Spring Boot
   - 使用Spring Data JPA
   - 实现参数校验
   - 实现全局异常处理
   - 实现统一响应格式

3. 测试阶段:
   - 编写单元测试
   - 编写集成测试
   - 使用Postman测试
   - 性能测试
   - 安全测试

4. 文档阶段:
   - 使用Swagger生成文档
   - 编写详细的注释
   - 提供示例代码
   - 提供错误码说明

5. 部署阶段:
   - 配置HTTPS
   - 配置CORS
   - 配置限流
   - 配置监控
   - 配置日志

6. 维护阶段:
   - 监控API性能
   - 分析错误日志
   - 优化慢接口
   - 及时更新文档
   - 处理安全问题

11.4 常见问题和解决方案

问题1:如何处理大量数据的查询?
解决方案:
- 使用分页
- 使用游标分页(大数据量)
- 使用字段过滤
- 使用缓存
- 异步处理

问题2:如何防止接口被恶意调用?
解决方案:
- 实现认证和授权
- 实现限流
- 实现防重放攻击
- 实现IP黑名单
- 实现验证码

问题3:如何保证API的向后兼容?
解决方案:
- 使用版本管理
- 新增字段而非修改字段
- 提供默认值
- 提供版本转换服务
- 提前通知废弃

问题4:如何优化API性能?
解决方案:
- 使用缓存
- 使用分页
- 使用字段过滤
- 使用异步处理
- 使用批量操作
- 数据库优化
- 使用CDN

问题5:如何处理并发问题?
解决方案:
- 使用乐观锁
- 使用悲观锁
- 使用分布式锁
- 使用消息队列
- 使用幂等性设计

11.5 学习路径建议

第一阶段:HTTP协议基础(1周)
- 学习HTTP协议原理
- 学习HTTP请求方法
- 学习HTTP状态码
- 学习HTTP请求头和响应头
- 学习HTTPS加密

第二阶段:RESTful设计(1周)
- 学习REST核心原则
- 学习URL设计规范
- 学习响应格式设计
- 学习错误处理
- 实践:设计用户管理API

第三阶段:Spring Boot实战(2周)
- 学习Spring Boot基础
- 学习Spring Data JPA
- 学习参数校验
- 学习全局异常处理
- 实践:实现商品管理API

第四阶段:API安全(1周)
- 学习JWT认证
- 学习权限控制
- 学习限流
- 学习防重放攻击
- 实践:实现安全机制

第五阶段:API文档和测试(1周)
- 学习Swagger
- 学习单元测试
- 学习集成测试
- 学习Postman
- 实践:完善文档和测试

第六阶段:综合项目(2周)
- 设计完整的电商API
- 实现所有功能模块
- 实现安全机制
- 编写文档和测试
- 部署上线

十二、总结

通过本文档的学习,你应该已经掌握了HTTP协议和RESTful API设计的核心知识。从HTTP协议的基础概念,到RESTful API的设计原则,再到实际项目中的安全、性能、测试等方面,我们进行了全面深入的讲解。

关键要点回顾:

  1. HTTP协议是Web通信的基础,理解其工作原理对于API开发至关重要
  2. RESTful是一种优雅的API设计风格,遵循其原则可以设计出清晰易用的API
  3. API安全不容忽视,认证、授权、限流、加密等措施缺一不可
  4. 良好的文档和测试是API质量的保证
  5. 实践是最好的学习方式,多动手实现才能真正掌握

继续学习建议:

  1. 深入学习Spring Security,掌握更多安全机制
  2. 学习微服务架构,了解分布式系统中的API设计
  3. 学习GraphQL,了解另一种API设计方式
  4. 学习API网关,了解API的统一管理
  5. 关注API设计的最新趋势和最佳实践

记住:优秀的API设计需要不断实践和总结,在实际项目中积累经验,才能设计出真正优秀的API。

祝你学习顺利,早日成为API设计高手!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐