HTTP协议与RESTful API设计
HTTP协议与RESTful API设计
学习目标
- 深入理解HTTP协议的工作原理和核心概念
- 掌握HTTP请求方法、状态码、请求头和响应头的使用
- 理解RESTful架构风格和设计原则
- 能够设计符合RESTful规范的API接口
- 掌握API版本管理、安全设计和最佳实践
- 能够在电商项目中设计和实现完整的商品API
- 理解HTTPS加密原理和安全通信机制
- 掌握API文档编写和接口测试方法
知识体系思维导图
一、HTTP协议基础
1.1 为什么需要HTTP协议?
在互联网世界中,客户端(浏览器、移动App)和服务器之间需要进行数据交换。但是:
问题场景:
用户在浏览器输入 www.example.com
↓
浏览器如何找到服务器?
浏览器如何告诉服务器要什么数据?
服务器如何返回数据?
数据用什么格式传输?
没有统一协议的问题:
- 每个应用都要自己定义通信格式
- 不同系统之间无法互通
- 开发效率低,维护成本高
- 安全性难以保证
HTTP协议的解决方案:
HTTP(HyperText Transfer Protocol)超文本传输协议
- 定义了客户端和服务器之间的通信规则
- 规定了请求和响应的格式
- 提供了统一的方法和状态码
- 支持多种数据类型传输
1.2 HTTP协议核心概念
1.2.1 请求-响应模型
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状态码选择流程图:
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:客户端发起请求
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版本管理?
**版本管理的必要性:**
场景:电商系统已上线,有大量用户在使用
需求变更:
- 商品价格字段从price改为priceInfo(包含原价、折扣价)
- 商品状态从字符串改为枚举
- 新增商品规格字段
问题:
- 直接修改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
要求:
- 实现用户的CRUD操作
- 使用RESTful风格设计URL
- 实现参数校验
- 实现统一响应格式
- 实现全局异常处理
/**
* 用户实体
*/
@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
要求:
- 实现订单的创建、查询、取消
- 实现订单状态流转
- 实现订单与用户的关联
- 实现订单与商品的关联
- 实现订单统计接口
/**
* 订单实体
*/
@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版本管理
要求:
- 实现V1和V2两个版本的商品API
- V1版本:简单的商品信息
- V2版本:增强的商品信息(包含规格、评分等)
- 实现版本转换服务
- 实现版本废弃警告
/**
* 任务:
* 1. 创建ProductV1Controller和ProductV2Controller
* 2. V1版本返回基本商品信息
* 3. V2版本返回增强商品信息
* 4. 实现版本转换服务(V1<->V2)
* 5. 为V1版本添加废弃警告头
* 6. 编写测试用例验证两个版本
*/
练习4:实现API安全机制
要求:
- 实现JWT认证
- 实现基于角色的权限控制
- 实现API限流
- 实现防重放攻击
- 实现敏感数据加密
/**
* 任务:
* 1. 实现JWT工具类(生成、验证Token)
* 2. 实现JWT认证过滤器
* 3. 实现权限注解和权限检查
* 4. 实现限流注解和限流切面
* 5. 实现防重放注解和防重放切面
* 6. 实现敏感字段加密(邮箱、手机号)
* 7. 编写测试用例验证安全机制
*/
练习5:实现API文档和测试
要求:
- 集成Swagger生成API文档
- 添加详细的API注解
- 编写Controller单元测试
- 编写集成测试
- 创建Postman测试集合
/**
* 任务:
* 1. 集成Swagger 3.0
* 2. 为所有Controller添加Swagger注解
* 3. 为所有DTO添加ApiModel注解
* 4. 编写Controller单元测试(使用MockMvc)
* 5. 编写集成测试(测试完整流程)
* 6. 创建Postman Collection
* 7. 编写Postman测试脚本
*/
9.3 综合练习
练习6:实现完整的电商API系统
要求:
- 用户管理:注册、登录、个人信息管理
- 商品管理:商品CRUD、分类管理、库存管理
- 购物车:添加商品、修改数量、删除商品
- 订单管理:创建订单、支付订单、查询订单
- 评论管理:发表评论、查询评论、删除评论
/**
* 系统架构:
*
* 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协议核心要点
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的设计原则,再到实际项目中的安全、性能、测试等方面,我们进行了全面深入的讲解。
关键要点回顾:
- HTTP协议是Web通信的基础,理解其工作原理对于API开发至关重要
- RESTful是一种优雅的API设计风格,遵循其原则可以设计出清晰易用的API
- API安全不容忽视,认证、授权、限流、加密等措施缺一不可
- 良好的文档和测试是API质量的保证
- 实践是最好的学习方式,多动手实现才能真正掌握
继续学习建议:
- 深入学习Spring Security,掌握更多安全机制
- 学习微服务架构,了解分布式系统中的API设计
- 学习GraphQL,了解另一种API设计方式
- 学习API网关,了解API的统一管理
- 关注API设计的最新趋势和最佳实践
记住:优秀的API设计需要不断实践和总结,在实际项目中积累经验,才能设计出真正优秀的API。
祝你学习顺利,早日成为API设计高手!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)