一、HTTP 请求的结构
在讨论怎么取参数之前,先看清参数藏在哪里。一个 HTTP 请求由三部分组成:请求行、请求头、请求体。
1.1 请求行
POST /api/v1/users/42/orders?page=1&size=20 HTTP/1.1
请求行包含三要素:
**请求方法:**POST,指定对资源的操作语义。
请求URL:/api/v1/users/42/orders?page=1&size=20,包含路径和查询字符串。
**协议版本:**HTTP/1.1,HTTP 协议版本号。
URL 中携带两种参数:
路径参数: 嵌在路径段中,如 /users/42 中的 42。
查询参数:? 后面的 key=value 键值对,多个用 & 连接。
1.2 请求头
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Accept: application/json
User-Agent: Mozilla/5.0
Cookie: sessionId=abc123
X-Forwarded-For: 192.168.1.100
请求头是 Key: Value 形式的元数据,不传输业务数据,但携带了格式声明、认证凭据、客户端信息等控制信息。常见请求头:
|-----------------|------------------------------|
| 请求头 | 作用 |
| Content-Type | 声明请求体的数据格式 |
| Authorization | 携带认证凭据(Bearer Token、Basic 等) |
| Accept | 告知服务器客户端期望的响应格式 |
| Cookie | 携带会话信息 |
| X-Forwarded-For | 代理场景下传递真实客户端 IP |
1.3 请求体
请求体是可选部分,GET 和 DELETE 请求通常没有请求体,POST / PUT / PATCH 通常有。请求体的格式由 Content-Type 请求头声明:
|-----------------------------------|-------------|------------------------|
| Content-Type | 格式 | 示例 |
| application/json | JSON | {"name":"张三","age":25} |
| application/x-www-form-urlencoded | 表单键值对 | name=张三&age=25 |
| multipart/form-data | 多部分表单(文件上传) | 二进制分片 |
| text/plain | 纯文本 | hello world |
1.4 完整请求结构
┌──────────────────────── 请求行 ────────────────────────┐
│ POST /api/v1/users/42/orders?page=1&size=20 HTTP/1.1 │
├──────────────────────── 请求头 ────────────────────────┤
│ Content-Type: application/json │
│ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... │
│ Accept: application/json │
├──────────────────────── 空行 ──────────────────────────┤
│ │
├──────────────────────── 请求体 ────────────────────────┤
│ { │
│ "productId": 1001, │
│ "quantity": 2, │
│ "remark": "不要辣" │
│ } │
└────────────────────────────────────────────────────────┘
我们一般会将参数放在以下四种位置:
路径参数 → 请求行 URL 路径段;查询参数 → 请求行 ? 后面;请求头参数 → 请求头区域;请求体参数 → 请求体区域。
二、方式一:路径参数 ------ @PathVariable
路径参数是 URL 路径中通过 {} 占位声明的变量,Spring 通过 @PathVariable 将其提取并绑定到方法参数。
2.1 基本用法
java
@GetMapping("/users/{id}")
public User getUser(@PathVariable("id") Long id) {
return userService.getById(id);
}
请求:GET /users/42 → id = 42
当方法参数名与占位符名一致时,可以省略 value(需编译时开启 -parameters):
java
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
2.2 多个路径参数
java
@DeleteMapping("/users/{userId}/orders/{orderId}")
public Result deleteOrder(@PathVariable Long userId,
@PathVariable Long orderId) {
orderService.delete(userId, orderId);
return Result.success();
}
请求:DELETE /users/42/orders/1001 → userId = 42, orderId = 1001
2.3 适用场景
路径参数用于标识资源或资源状态,属于 URL 路径的一部分,语义上是"你要操作哪个东西":
|--------|--------------------------------|--------------------------------|
| 场景 | 示例 URL | 路径参数 |
| 获取单个资源 | GET /users/42 | id=42 |
| 切换状态 | PUT /orders/1001/status/2 | id=1001, status=2 |
| 嵌套资源操作 | DELETE /categories/5/dishes/10 | DELETE /categories/5/dishes/10 |
三、方式二:查询参数 ------ @RequestParam / 省略注解
查询参数是 URL 中 ? 后面的 key=value 键值对,Spring 提供了多种接收方式。
3.1 显式指定:@RequestParam
java
@GetMapping("/products")
public List<Product> list(@RequestParam("category") String category,
@RequestParam("page") int page) {
return productService.list(category, page);
}
请求:GET /products?category=electronics&page=2
@RequestParam 的三个常用属性:
java
@GetMapping("/search")
public Result search(
@RequestParam(value = "keyword", required = true) String keyword,
@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "size", defaultValue = "20") int size
) { ... }
3.2 省略注解
当方法参数是简单类型(基本类型及其包装类、String、Date 等)且不加任何注解时,Spring 默认按查询参数名匹配:
java
@GetMapping("/products")
public List<Product> list(String keyword, int page) {
// 等价于 @RequestParam("keyword") 和 @RequestParam("page")
return productService.list(keyword, page);
}
**注意:**省略注解要求方法参数名与 URL 查询参数名一致。这依赖编译时的 -parameters 选项。如果未开启,Spring 无法通过反射获取参数名,会报错。Maven 配置:
XML
> <plugin>
> <groupId>org.apache.maven.plugins</groupId>
> <artifactId>maven-compiler-plugin</artifactId>
> <configuration>
> <parameters>true</parameters>
> </configuration>
> </plugin>
>
3.3 对象封装:POJO 接收多个查询参数
查询参数多的时候,逐个声明方法参数会很冗长。Spring 支持将查询参数自动封装到 POJO 中:
java
@GetMapping("/products")
public PageResult<Product> list(ProductQueryDTO query) {
return productService.pageQuery(query);
}
java
@Data
public class ProductQueryDTO {
private String keyword;
private Integer categoryId;
private int page = 1;
private int size = 20;
private String sortField;
private String sortOrder;
}
请求:GET /products?keyword=手机&categoryId=5&page=2&size=10
Spring 遍历查询参数,按名称匹配 POJO 的字段,通过 setter 注入。POJO 要求:有无参构造器 + 有标准 setter(Lombok @Data 满足)。
3.4 接收数组和集合
查询参数允许同名多值,Spring 可以用数组或 List 接收:
java
// 请求:GET /products?tag=电子&tag=热销&tag=新品
@GetMapping("/products")
public List<Product> list(@RequestParam("tag") List<String> tags) {
return productService.findByTags(tags);
}
也可以用 @RequestParam 接收 Map<String, String> 获取所有查询参数:
java
@GetMapping("/search")
public Result search(@RequestParam Map<String, String> params) {
params.forEach((k, v) -> System.out.println(k + "=" + v));
return Result.success();
}
3.5 适用场景
查询参数用于筛选、分页、排序、搜索等,语义上是"你要什么样的资源":
|-------|------------------------------------------|
| 场景 | 示例 URL |
| 分页查询 | GET /users?page=1&size=20 |
| 条件筛选 | GET /products?category=电子&priceMax=5000 |
| 关键字搜索 | GET /search?keyword=手机 |
| 排序 | GET /products?sort=price&order=desc |
四、方式三:请求体参数 ------ @RequestBody
请求体参数位于 HTTP 请求的 body 区域,通常是 JSON 格式。Spring 通过 @RequestBody 触发 HttpMessageConverter(默认 Jackson 的 MappingJackson2HttpMessageConverter)将 JSON 反序列化为 Java 对象。
4.1 基本用法
java
@PostMapping("/users")
public Result createUser(@RequestBody UserDTO userDTO) {
userService.create(userDTO);
return Result.success();
}
请求:
java
POST /users
Content-Type: application/json
{
"username": "zhangsan",
"email": "zhangsan@example.com",
"age": 25
}
Spring 读取请求体中的 JSON 字节流,通过 Jackson 将其反序列化为 UserDTO 对象。反序列化要求:
POJO 有无参构造器
字段名与 JSON key 一致(或通过 @JsonProperty 映射)
有标准 setter(或使用 Lombok @Data)
4.2 JSON 字段名与 Java 字段名不一致时
用 @JsonProperty 做映射:
java
@Data
public class UserDTO {
@JsonProperty("user_name")
private String userName;
@JsonProperty("email_address")
private String emailAddress;
}
前端传:
java
{
"user_name": "张三",
"email_address": "zhangsan@qq.com"
}
4.3 接收嵌套 JSON
java
@Data
public class OrderDTO {
private Long addressId;
private String remark;
private List<OrderItemDTO> items; // 嵌套对象列表
}
@Data
public class OrderItemDTO {
private Long productId;
private Integer quantity;
}
前端传:
java
{
"addressId": 1,
"remark": "不要辣",
"items": [
{"productId": 1001, "quantity": 2},
{"productId": 1002, "quantity": 1}
]
}
Jackson 会递归反序列化嵌套对象,无需额外注解。
4.4 接收非 JSON 格式的请求体
@RequestBody 不仅仅用于 JSON。Spring 会根据 Content-Type 选择对应的 HttpMessageConverter:
|------------------|----------------------------------------|------------|
| Content-Type | 使用的 Converter | 用法 |
| application/json | MappingJackson2HttpMessageConverter | 反序列化为 POJO |
| application/xml | MappingJackson2XmlHttpMessageConverter | 反序列化为 POJO |
| text/plain | StringHttpMessageConverter | 接收为 String |
java
// 接收纯文本请求体
@PostMapping("/notify")
public Result handleNotify(@RequestBody String payload) {
log.info("收到通知:{}", payload);
return Result.success();
}
4.5 @RequestBody 不能省略
这是一个高频踩坑点。如果省略 @RequestBody:
java
// 错误:省略了 @RequestBody
@PostMapping("/users")
public Result createUser(UserDTO userDTO) {
// userDTO 的所有字段都是 null
return Result.success();
}
Spring 对没有 @RequestBody 注解的复杂类型参数,执行的是表单参数绑定 (WebDataBinder),即从 application/x-www-form-urlencoded 格式的查询参数 / 表单参数中按字段名匹配。前端传的是 JSON 请求体,Spring 根本不会去读它,结果所有字段为 null。
**规则:**只要前端用 application/json 传数据,Controller 方法参数必须加 @RequestBody,没有例外。
4.6 一个方法只能有一个 @RequestBody
HTTP 请求体只能被读取一次,Spring 不支持用多个 @RequestBody 参数分别接收不同部分。如果需要传递多个逻辑对象,封装到一个外层 DTO:
java
// 错误:两个 @RequestBody
@PostMapping("/order")
public Result create(@RequestBody OrderDTO order, @RequestBody PaymentDTO payment) { ... }
// 正确:封装到外层 DTO
@Data
public class CreateOrderRequest {
private OrderDTO order;
private PaymentDTO payment;
}
@PostMapping("/order")
public Result create(@RequestBody CreateOrderRequest request) { ... }
4.7 适用场景
请求体参数用于提交、修改复杂数据,语义上是**"我要给你什么数据"**:
|------|--------------------|-------------|
| 场景 | HTTP 方法 | 说明 |
| 新增资源 | POST + JSON | 创建用户、提交订单 |
| 修改资源 | PUT / PATCH + JSON | 修改个人信息、更新配置 |
| 批量操作 | POST + JSON 数组 | 批量导入、批量删除 |
五、方式四:请求头参数 ------ @RequestHeader
请求头中的值通过 @RequestHeader 提取。虽然业务数据一般不放在请求头中,但认证凭据、追踪标识、客户端信息等经常通过请求头传递。
5.1 基本用法
java
@GetMapping("/profile")
public Result getProfile(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
Long userId = jwtUtil.parseUserId(token);
return Result.success(userService.getById(userId));
}
请求:
java
GET /profile HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
5.2 常用属性
@RequestHeader 与 @RequestParam 的属性完全一致:
|--------------|----------|----------------|
| 属性 | 作用 | 默认值 |
| value / name | 请求头的 key | 方法参数名 |
| required | 是否必传 | true(不存在时抛 400 |
| defaultValue | 未传时的默认值 | 无 |
5.3 获取所有请求头
如果需要获取多值请求头(如 Accept 可以有多个值),使用 MultiValueMap:
java
@GetMapping("/headers")
public Result headers(@RequestHeader MultiValueMap<String, String> headers) {
List<String> acceptValues = headers.get("Accept");
return Result.success();
}
5.4 拦截器中手动获取请求头
在认证拦截器中,通常不用 @RequestHeader,而是直接从 HttpServletRequest 获取:
java
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("Authorization");
if (token == null || !jwtUtil.validate(token)) {
response.setStatus(401);
return false;
}
return true;
}
}
5.5 适用场景
请求头参数不是业务数据,而是控制信息:
|-------|------------------------------|
| 场景 | 请求头 |
| 身份认证 | Authorization: Bearer xxx |
| 请求追踪 | X-Trace-Id: abc-123 |
| 多语言支持 | Accept-Language: zh-CN |
| 内容协商 | Accept: application/json |
| 客户端标识 | X-Device-Type: iOS |
| 幂等性控制 | Idempotency-Key: unique-uuid |
六、总结
|-------|--------------------|---------|------------|
| 参数方式 | 注解 | 从哪取 | 传什么 |
| 路径参数 | @PathVariable | URL 路径段 | 资源标识 |
| 查询参数 | @RequestParam / 省略 | URL ? 后 | 筛选、分页、排序 |
| 请求体参数 | @RequestBody | 请求体 | 提交的复杂数据 |
| 请求头参数 | @RequestHeader | 请求头 | 认证、追踪等控制信息 |
记住一个核心判断逻辑:
要定位资源 → 路径参数
要筛选资源 → 查询参数
要提交数据 → 请求体参数(加 @RequestBody)
要传递控制信息 → 请求头参数