在 SpringBoot 开发中,处理 HTTP 请求参数是我们每天都要面对的工作。而@RequestParam
和@RequestBody
这两个注解,就像是我们手中的两把利剑,既能高效解决问题,用不好也可能 "误伤" 自己。
作为一名资深 Java 开发者,我见过太多因为对这两个注解理解不透彻而导致的生产事故:有人把 JSON 参数用@RequestParam
接收导致接口一直报 400 错误,有人在 GET 请求里用@RequestBody
接收参数结果百思不得其解,还有人因为不清楚参数绑定的优先级而写出难以维护的代码...
本文将从基础到进阶,全方位剖析这两个注解的使用场景、底层原理、常见问题和最佳实践,带你彻底掌握它们的精髓,让你的接口参数处理代码既优雅又健壮!
一、初识 @RequestParam:URL 中的参数捕获器
@RequestParam
是 SpringMVC 中最常用的注解之一,用于从 HTTP 请求的参数中提取数据并绑定到控制器方法的参数上。它看似简单,实则暗藏玄机。
1.1 @RequestParam 的基本用法
@RequestParam
主要用于获取 HTTP 请求中的查询参数(query parameters),也就是 URL 中?
后面的键值对。例如在http://localhost:8080/user?name=张三&age=20
这个 URL 中,name
和age
就是查询参数。
基本语法如下:
java
@RequestMapping("/user")
public String getUser(@RequestParam String name,@RequestParam int age) {return "姓名:" + name + ",年龄:" + age;
}
这段代码会自动从请求参数中获取name
和age
的值,并分别赋值给方法参数。
1.2 @RequestParam 的属性详解
@RequestParam
注解有三个重要属性,掌握它们能让你更灵活地使用这个注解:
value
/name
:指定请求参数的名称,如果方法参数名与请求参数名一致,可以省略required
:表示该参数是否必须,默认值为true
defaultValue
:指定参数的默认值,当参数不存在时使用
下面通过示例详细说明:
1.2.1 指定参数名称
当方法参数名与请求参数名不一致时,需要通过value
或name
属性指定:
java
@RequestMapping("/user")
public String getUser(@RequestParam(value = "userName") String name, // 请求参数是userName,绑定到name变量@RequestParam(name = "userAge") int age) { // 请求参数是userAge,绑定到age变量return "姓名:" + name + ",年龄:" + age;
}
此时,我们需要用http://localhost:8080/user?userName=张三&userAge=20
来访问接口。
1.2.2 处理非必需参数
当required
属性设为false
时,表示该参数不是必需的:
java
@RequestMapping("/search")
public String search(@RequestParam String keyword,@RequestParam(required = false) Integer page) { // page参数非必需// 如果page为null,则默认查询第一页int currentPage = (page == null) ? 1 : page;return "搜索关键词:" + keyword + ",页码:" + currentPage;
}
这个接口可以通过http://localhost:8080/search?keyword=java
(不带 page 参数)访问,也可以通过http://localhost:8080/search?keyword=java&page=2
访问。
如果一个required=true
的参数未提供,Spring 会抛出MissingServletRequestParameterException
异常,导致接口返回 400 Bad Request 错误。
1.2.3 设置默认值
defaultValue
属性可以为参数设置默认值,当参数不存在时自动使用该值:
java
@RequestMapping("/search")
public String search(@RequestParam String keyword,@RequestParam(defaultValue = "1") int page, // 默认页码为1@RequestParam(defaultValue = "10") int size) { // 默认每页10条return "搜索关键词:" + keyword + ",页码:" + page + ",每页条数:" + size;
}
这个接口有以下几种访问方式:
http://localhost:8080/search?keyword=java
→ page=1, size=10http://localhost:8080/search?keyword=java&page=2
→ page=2, size=10http://localhost:8080/search?keyword=java&page=3&size=20
→ page=3, size=20
需要注意的是,设置了defaultValue
后,required
属性会自动变为false
,即使你显式设置required=true
也会被忽略。
1.3 @RequestParam 处理数组和集合
@RequestParam
不仅能处理基本类型,还能直接绑定数组和集合类型的参数。
1.3.1 处理数组
当请求参数有多个相同名称时,可以用数组接收:
java
@RequestMapping("/array")
public String handleArray(@RequestParam String[] hobbies) {return "爱好:" + Arrays.toString(hobbies);
}
访问http://localhost:8080/array?hobbies=读书&hobbies=运动&hobbies=编程
,会得到结果:爱好:[读书, 运动, 编程]
1.3.2 处理集合
处理集合时,需要指定value
属性,并且最好指定required
属性:
java
@RequestMapping("/list")
public String handleList(@RequestParam(value = "ids", required = false) List<Integer> ids) {return "IDs:" + ids;
}
访问http://localhost:8080/list?ids=1&ids=2&ids=3
,会得到结果:IDs:[1, 2, 3]
如果需要处理集合,Spring 需要知道集合的泛型类型。对于基本类型,Spring 可以自动推断,但对于自定义类型,可能需要额外配置。
1.4 @RequestParam 的使用场景
@RequestParam
适用于以下场景:
- GET 请求参数:GET 请求的参数通常放在 URL 的查询字符串中,这是
@RequestParam
最主要的应用场景 - 表单提交的参数:表单默认使用
application/x-www-form-urlencoded
格式提交,其参数也可以用@RequestParam
接收 - 简单参数传递:适用于传递简单类型的数据(字符串、数字等)
- URL 路径中的查询参数:无论 HTTP 方法是什么,只要参数在 URL 的查询字符串中,都可以用
@RequestParam
接收
1.5 @RequestParam 的常见问题与解决方案
1.5.1 参数类型不匹配
当请求参数的类型与方法参数的类型不匹配时,会抛出TypeMismatchException
异常。
例如,下面的接口期望接收一个整数:
java
@RequestMapping("/number")
public String handleNumber(@RequestParam int number) {return "数字:" + number;
}
如果我们用http://localhost:8080/number?number=abc
访问,会得到 400 错误,因为 "abc" 无法转换为整数。
解决方案:
- 使用包装类型(如
Integer
代替int
),这样当参数类型不匹配时会得到null
而不是抛出异常 - 添加参数校验
- 使用
@ExceptionHandler
全局处理类型转换异常
java
// 改进方案
@RequestMapping("/number")
public String handleNumber(@RequestParam(required = false) Integer number) {if (number == null) {return "请提供有效的数字参数";}return "数字:" + number;
}
1.5.2 中文乱码问题
当请求参数包含中文时,可能会出现乱码问题。这通常是由于 URL 编码与服务器解码使用的字符集不一致导致的。
解决方案:
- 确保请求 URL 的中文参数进行了正确编码(通常是 UTF-8)
- 在 SpringBoot 中配置字符编码过滤器
java
@Configuration
public class WebConfig implements WebMvcConfigurer {@Beanpublic FilterRegistrationBean<CharacterEncodingFilter> characterEncodingFilter() {FilterRegistrationBean<CharacterEncodingFilter> registrationBean = new FilterRegistrationBean<>();CharacterEncodingFilter filter = new CharacterEncodingFilter();filter.setEncoding("UTF-8");filter.setForceEncoding(true);registrationBean.setFilter(filter);registrationBean.addUrlPatterns("/*");return registrationBean;}
}
在 SpringBoot 2.x 及以上版本,默认已经配置了 UTF-8 编码,一般不需要额外配置,但如果出现乱码问题,上述配置可以作为解决方案。
1.5.3 参数名与 Java 关键字冲突
有时请求参数名可能与 Java 关键字重名(如int
、class
等),这时候直接使用关键字作为方法参数名会导致编译错误。
解决方案:使用@RequestParam
的value
属性指定参数名,方法参数名使用其他合法名称
java
@RequestMapping("/keyword")
public String handleKeyword(@RequestParam(value = "class") String className) {return "班级:" + className;
}
这样就可以处理http://localhost:8080/keyword?class=一班
这样的请求了。
二、深入 @RequestBody:请求体的解析能手
@RequestBody
注解用于将 HTTP 请求的正文(body)解析并绑定到控制器方法的参数上。它主要用于处理非application/x-www-form-urlencoded
格式的请求数据,如 JSON、XML 等。
2.1 @RequestBody 的基本用法
@RequestBody
通常用于 POST、PUT 等 HTTP 方法,这些方法的参数通常放在请求体中而不是 URL 中。
基本语法如下:
java
@PostMapping("/user")
public String createUser(@RequestBody User user) {return "创建用户:" + user.getName() + ",年龄:" + user.getAge();
}// User类定义
public class User {private String name;private int age;// 必须有默认构造函数public User() {}// getter和setter方法public String getName() { return name; }public void setName(String name) { this.name = name; }public int getAge() { return age; }public void setAge(int age) { this.age = age; }
}
当我们向http://localhost:8080/user
发送 POST 请求,并且请求体是 JSON 格式:
json
{"name": "张三","age": 20
}
Spring 会自动将 JSON 数据解析为 User 对象,并传递给 createUser 方法。
2.2 @RequestBody 支持的数据格式
@RequestBody
支持多种数据格式,这取决于项目中配置的消息转换器(MessageConverter)。SpringBoot 默认配置了以下几种常用的消息转换器:
- JSON:通过 Jackson 库支持,是最常用的格式
- XML:通过 JAXB 支持,如果添加了相应依赖
- Form 表单:
application/x-www-form-urlencoded
格式,但通常用@RequestParam
处理 - Multipart:
multipart/form-data
格式,用于文件上传
要使用特定的数据格式,需要确保请求头中的Content-Type
与实际数据格式一致:
- JSON:
Content-Type: application/json
- XML:
Content-Type: application/xml
或text/xml
2.3 @RequestBody 与简单类型
虽然@RequestBody
主要用于绑定复杂对象,但也可以用于绑定简单类型,如字符串、数字等:
java
@PostMapping("/message")
public String handleMessage(@RequestBody String message) {return "收到消息:" + message;
}
当发送包含纯文本的请求体时,这段代码会直接将文本内容绑定到 message 参数。
但需要注意的是,@RequestBody
一次只能绑定一个简单类型参数,因为整个请求体只能解析为一个值。如果需要传递多个简单参数,应该使用对象包装它们,或者考虑使用@RequestParam
。
2.4 @RequestBody 与集合类型
@RequestBody
可以直接绑定集合类型,例如List
、Map
等:
2.4.1 绑定 List
java
@PostMapping("/users")
public String createUsers(@RequestBody List<User> users) {return "创建用户数量:" + users.size() + ",第一个用户:" + users.get(0).getName();
}
对应的 JSON 请求体:
json
[{"name": "张三", "age": 20},{"name": "李四", "age": 22}
]
2.4.2 绑定 Map
java
@PostMapping("/info")
public String handleInfo(@RequestBody Map<String, Object> info) {return "姓名:" + info.get("name") + ",爱好:" + info.get("hobby");
}
对应的 JSON 请求体:
json
{"name": "张三","hobby": "编程"
}
2.5 @RequestBody 的使用场景
@RequestBody
适用于以下场景:
- POST/PUT 请求:这些请求通常需要传递复杂的数据,适合放在请求体中
- JSON/XML 数据:处理结构化的复杂数据时,
@RequestBody
能自动完成对象映射 - RESTful API:在 RESTful 风格的 API 中,创建和更新资源通常使用
@RequestBody
接收数据 - 传递大量数据:当需要传递的数据量较大时,放在请求体中比放在 URL 中更合适
- 复杂对象结构:当参数是多层嵌套的复杂对象时,
@RequestBody
能简化参数绑定
2.6 @RequestBody 的常见问题与解决方案
2.6.1 400 Bad Request 错误
这是使用@RequestBody
时最常见的错误,通常有以下几种原因:
- 请求体为空:当
@RequestBody
标注的参数是必需的,但请求体为空时 - JSON 格式错误:请求体的 JSON 格式不正确(如缺少引号、括号不匹配等)
- 类型不匹配:JSON 中的字段类型与 Java 对象的字段类型不匹配
- 缺少默认构造函数:Java 对象没有提供默认的无参构造函数
解决方案:
- 对于非必需参数,可以结合
required
属性(默认为 true):
java
@PostMapping("/user")
public String createUser(@RequestBody(required = false) User user) {if (user == null) {return "未提供用户信息";}return "创建用户:" + user.getName();
}
- 确保 JSON 格式正确,可以使用 JSON 校验工具验证
- 检查 Java 对象是否有默认构造函数
- 添加全局异常处理,捕获并处理 JSON 解析异常:
java
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(HttpMessageNotReadableException.class)public String handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {return "请求数据格式错误:" + e.getMessage();}
}
2.6.2 字段不匹配问题
当 JSON 中的字段名与 Java 对象的字段名不一致时,会导致字段无法正确绑定。
解决方案:使用 Jackson 的@JsonProperty
注解指定映射关系
java
public class User {@JsonProperty("user_name") // 与JSON中的user_name字段对应private String name;@JsonProperty("user_age") // 与JSON中的user_age字段对应private int age;// getter、setter和构造函数省略
}
这样,即使 JSON 中的字段名与 Java 对象不同,也能正确绑定:
json
{"user_name": "张三","user_age": 20
}
2.6.3 日期类型处理
日期类型的 JSON 字符串(如 "2023-10-01" 或 "2023/10/01 12:00:00")在默认情况下可能无法正确解析为 Java 的Date
或LocalDateTime
类型。
解决方案:
- 使用
@JsonFormat
注解指定日期格式:
java
public class Order {private String orderId;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime createTime;// getter、setter和构造函数省略
}
- 配置全局日期格式(推荐):
java
@Configuration
public class JacksonConfig {@Beanpublic ObjectMapper objectMapper() {ObjectMapper objectMapper = new ObjectMapper();JavaTimeModule javaTimeModule = new JavaTimeModule();// 配置LocalDateTime的序列化和反序列化格式javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));objectMapper.registerModule(javaTimeModule);// 忽略未知属性,避免因JSON中有额外字段而报错objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);return objectMapper;}
}
三、@RequestParam 与 @RequestBody 的终极对比
虽然@RequestParam
和@RequestBody
都是用于获取请求参数的注解,但它们在很多方面都有显著区别。理解这些区别是正确使用它们的关键。
3.1 数据位置不同
这是两者最根本的区别:
@RequestParam
:获取 URL 查询参数(query parameters)或表单提交的参数(application/x-www-form-urlencoded
)@RequestBody
:获取 HTTP 请求体(request body)中的数据
形象地说,@RequestParam
处理的是 URL 中?
后面的数据,而@RequestBody
处理的是 HTTP 请求中独立于 URL 的正文部分。
3.2 支持的 HTTP 方法不同
@RequestParam
:通常用于 GET 方法,也可以用于 POST、PUT 等方法(当参数在 URL 或表单中时)@RequestBody
:通常用于 POST、PUT、PATCH 等方法,不建议用于 GET 方法(GET 方法通常没有请求体)
虽然 HTTP 规范并没有禁止 GET 方法有请求体,但大多数浏览器和服务器对 GET 请求体的支持并不完善,因此实际开发中应避免在 GET 方法中使用@RequestBody
。
3.3 数据格式不同
@RequestParam
:主要处理键值对形式的数据(key1=value1&key2=value2
)@RequestBody
:主要处理结构化数据,如 JSON、XML 等
@RequestParam
可以处理表单提交的application/x-www-form-urlencoded
格式数据,而@RequestBody
可以处理更复杂的结构化数据。
3.4 参数数量不同
@RequestParam
:可以同时获取多个参数,每个参数对应 URL 或表单中的一个键值对@RequestBody
:通常一次只能绑定一个参数,因为整个请求体只能解析为一个对象
如果需要获取多个参数,@RequestBody
通常会将这些参数封装到一个 Java 对象中。
3.5 适用场景不同
场景 | 推荐使用 | 原因 |
---|---|---|
获取简单参数 | @RequestParam | 简单直接,无需额外对象 |
表单提交 | @RequestParam | 表单默认使用 application/x-www-form-urlencoded 格式 |
RESTful API 创建资源 | @RequestBody | 适合传递复杂对象,符合 REST 风格 |
传递大量数据 | @RequestBody | URL 长度有限制,请求体可以容纳更多数据 |
传递复杂对象 | @RequestBody | 自动映射复杂对象,包括嵌套结构 |
分页查询参数 | @RequestParam | 分页参数通常是简单值,适合放在 URL 中 |
搜索过滤条件 | @RequestParam 或 @RequestBody | 简单条件用 @RequestParam,复杂条件用 @RequestBody |
3.6 混合使用示例
在实际开发中,我们经常需要同时使用@RequestParam
和@RequestBody
:
java
@PostMapping("/orders")
public String createOrder(@RequestParam String userId, // 从URL参数获取用户ID@RequestParam(required = false) String couponCode, // 从URL参数获取优惠券代码(可选)@RequestBody OrderRequest orderRequest) { // 从请求体获取订单详情return "用户ID:" + userId + ",优惠券:" + (couponCode != null ? couponCode : "无") + ",订单商品:" + orderRequest.getProducts().size() + "件";
}// OrderRequest类
public class OrderRequest {private List<Product> products;private String shippingAddress;private String paymentMethod;// getter、setter和构造函数省略
}
这个接口可以通过以下方式调用:
- URL:
http://localhost:8080/orders?userId=123&couponCode=SAVE10
- 请求方法:POST
- Content-Type:application/json
- 请求体:
json
{"products": [{"id": "p1", "name": "手机", "quantity": 1},{"id": "p2", "name": "耳机", "quantity": 2}],"shippingAddress": "北京市朝阳区","paymentMethod": "支付宝"
}
这种混合使用的方式结合了两种注解的优势,适用于既需要简单参数又需要复杂对象的场景。
四、高级应用与最佳实践
掌握了@RequestParam
和@RequestBody
的基本用法后,我们来看看一些高级应用和最佳实践,帮助你写出更优雅、更健壮的代码。
4.1 参数校验
无论是@RequestParam
还是@RequestBody
绑定的参数,都需要进行合法性校验,以确保系统安全和数据正确。
Spring 支持 JSR-303/JSR-380 规范的参数校验,可以通过注解轻松实现:
java
@PostMapping("/user")
public String createUser(@RequestParam @NotBlank(message = "用户名不能为空") String username,@RequestParam @Min(value = 18, message = "年龄不能小于18岁") int age,@RequestBody @Valid UserDetail detail) {return "创建用户:" + username;
}// UserDetail类
public class UserDetail {@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不正确")private String email;@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")private String password;// getter、setter和构造函数省略
}
要使用参数校验,需要添加以下依赖:
xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
并在全局异常处理器中处理校验失败的异常:
java
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)public String handleMethodArgumentNotValid(MethodArgumentNotValidException e) {// 获取所有校验失败的消息List<String> errorMessages = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.toList());return "参数校验失败:" + String.join(";", errorMessages);}@ExceptionHandler(ConstraintViolationException.class)public String handleConstraintViolation(ConstraintViolationException e) {List<String> errorMessages = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());return "参数校验失败:" + String.join(";", errorMessages);}
}
4.2 自定义参数解析器
当@RequestParam
和@RequestBody
不能满足需求时,我们可以自定义参数解析器来处理特殊的参数绑定逻辑。
例如,我们可以创建一个@CurrentUser
注解,用于直接获取当前登录用户:
java
// 自定义注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}// 自定义参数解析器
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {@Overridepublic boolean supportsParameter(MethodParameter parameter) {// 只处理带有@CurrentUser注解的User类型参数return parameter.hasParameterAnnotation(CurrentUser.class) && parameter.getParameterType().equals(User.class);}@Overridepublic Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {// 从请求中获取当前登录用户ID(实际项目中可能从Token、Session等获取)String userId = webRequest.getHeader("X-User-Id");// 根据用户ID查询用户信息(实际项目中可能是从数据库或缓存获取)User currentUser = new User();currentUser.setId(userId);currentUser.setName("当前登录用户");return currentUser;}
}// 注册解析器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {resolvers.add(new CurrentUserArgumentResolver());}
}// 使用示例
@GetMapping("/profile")
public String getProfile(@CurrentUser User currentUser) {return "当前登录用户:" + currentUser.getName() + ",ID:" + currentUser.getId();
}
这个例子展示了如何通过自定义参数解析器扩展 Spring 的参数绑定能力,在实际项目中非常实用。
4.3 处理文件上传
文件上传是 Web 开发中的常见需求,Spring 提供了@RequestParam
结合MultipartFile
来处理文件上传:
java
@PostMapping("/upload")
public String uploadFile(@RequestParam String description, // 普通参数@RequestParam("file") MultipartFile file) { // 文件参数if (file.isEmpty()) {return "请选择要上传的文件";}try {// 获取文件名String fileName = file.getOriginalFilename();// 获取文件内容byte[] bytes = file.getBytes();// 保存文件(实际项目中通常保存到磁盘或云存储)Path path = Paths.get("uploads/" + fileName);Files.write(path, bytes);return "文件上传成功:" + fileName + ",描述:" + description;} catch (IOException e) {return "文件上传失败:" + e.getMessage();}
}
多文件上传可以使用MultipartFile
数组:
java
@PostMapping("/upload-multiple")
public String uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {List<String> uploadedFiles = new ArrayList<>();for (MultipartFile file : files) {if (!file.isEmpty()) {try {String fileName = file.getOriginalFilename();Path path = Paths.get("uploads/" + fileName);Files.write(path, file.getBytes());uploadedFiles.add(fileName);} catch (IOException e) {return "文件" + file.getOriginalFilename() + "上传失败:" + e.getMessage();}}}return "成功上传文件:" + String.join(",", uploadedFiles);
}
需要注意的是,文件上传的表单需要设置enctype="multipart/form-data"
属性:
html
预览
<form method="post" action="/upload" enctype="multipart/form-data"><input type="text" name="description" /><input type="file" name="file" /><button type="submit">上传</button>
</form>
在 SpringBoot 中,默认的文件上传大小限制可能比较小,可以通过配置修改:
properties
# 单个文件大小限制
spring.servlet.multipart.max-file-size=10MB
# 总请求大小限制
spring.servlet.multipart.max-request-size=100MB
4.4 RESTful API 设计中的最佳实践
在 RESTful API 设计中,合理使用@RequestParam
和@RequestBody
能让 API 更加规范和易用:
GET 请求:使用
@RequestParam
获取查询参数,用于查询、过滤、分页等操作java
@GetMapping("/users") public Page<User> getUsers(@RequestParam(required = false) String name, // 可选的姓名过滤@RequestParam(defaultValue = "0") int page, // 页码,默认0@RequestParam(defaultValue = "10") int size) { // 每页条数,默认10// 查询逻辑 }
POST 请求:使用
@RequestBody
创建资源,请求体包含资源的完整信息java
@PostMapping("/users") public User createUser(@RequestBody @Valid UserCreateRequest request) {// 创建用户逻辑 }
PUT 请求:使用
@RequestBody
更新资源,通常包含资源的完整信息java
@PutMapping("/users/{id}") public User updateUser(@PathVariable String id, // 路径参数,用户ID@RequestBody @Valid UserUpdateRequest request) {// 更新用户逻辑 }
PATCH 请求:使用
@RequestBody
部分更新资源,通常只包含需要更新的字段java
@PatchMapping("/users/{id}") public User partialUpdateUser(@PathVariable String id,@RequestBody Map<String, Object> updates) { // 只包含需要更新的字段// 部分更新逻辑 }
DELETE 请求:通常使用路径参数指定要删除的资源 ID,复杂情况可结合
@RequestParam
java
@DeleteMapping("/users/{id}") public void deleteUser(@PathVariable String id) {// 删除用户逻辑 }
遵循这些实践可以使你的 API 更加直观和易用,符合 RESTful 设计原则。
五、底层原理探秘
了解@RequestParam
和@RequestBody
的底层原理,不仅能帮助我们更好地理解它们的行为,还能在遇到问题时更快地定位原因。
5.1 SpringMVC 的请求处理流程
SpringMVC 处理一个 HTTP 请求的大致流程如下:
- 客户端发送 HTTP 请求到 DispatcherServlet
- DispatcherServlet 根据请求 URL 查找对应的 Handler(通常是控制器方法)
- 找到 Handler 后,DispatcherServlet 调用 HandlerAdapter 处理请求
- HandlerAdapter 负责解析请求参数,并绑定到 Handler 的方法参数上
- 执行 Handler 方法,得到返回结果
- HandlerAdapter 将返回结果封装成 ModelAndView
- DispatcherServlet 根据 ModelAndView 选择合适的 ViewResolver 渲染视图
- 将渲染结果返回给客户端
@RequestParam
和@RequestBody
的处理发生在第 4 步,由特定的参数解析器完成。
5.2 @RequestParam 的解析原理
@RequestParam
的解析主要由RequestParamMethodArgumentResolver
类完成,其工作流程如下:
- 检查方法参数是否标注了
@RequestParam
注解 - 如果标注了,从请求对象(HttpServletRequest)中获取参数值:
- 首先调用
request.getParameter(name)
获取参数值 - 这方法会同时查找查询参数和表单参数(
application/x-www-form-urlencoded
)
- 首先调用
- 如果参数是数组或集合,将获取到的多个值转换为对应的数组或集合
- 进行类型转换,将字符串参数转换为方法参数的类型
- 将转换后的值绑定到方法参数上
request.getParameter(name)
方法的底层实现会根据请求的Content-Type
不同而有不同的处理逻辑:
- 对于 GET 请求,从 URL 的查询字符串中解析参数
- 对于 POST 请求且
Content-Type
为application/x-www-form-urlencoded
,从请求体中解析参数 - 其他类型的请求,主要从 URL 的查询字符串中解析参数
5.3 @RequestBody 的解析原理
@RequestBody
的解析主要由RequestResponseBodyMethodProcessor
类完成,其工作流程如下:
- 检查方法参数是否标注了
@RequestBody
注解 - 从请求对象中获取输入流(InputStream)
- 根据请求头中的
Content-Type
选择合适的 HttpMessageConverter - 使用选中的 HttpMessageConverter 将请求体内容转换为方法参数的类型
- 将转换后的对象绑定到方法参数上
HttpMessageConverter 是一个接口,不同的实现类处理不同的数据格式:
MappingJackson2HttpMessageConverter
:处理 JSON 格式(默认包含)Jaxb2RootElementHttpMessageConverter
:处理 XML 格式(需要相应依赖)StringHttpMessageConverter
:处理字符串格式FormHttpMessageConverter
:处理表单数据
Spring 会根据Content-Type
请求头和目标类型来选择最合适的 HttpMessageConverter。
5.4 参数解析器的优先级
SpringMVC 中有多种参数解析器,它们有不同的优先级:
RequestParamMethodArgumentResolver
(处理@RequestParam
)RequestResponseBodyMethodProcessor
(处理@RequestBody
)- 其他解析器(如处理路径参数的
PathVariableMethodArgumentResolver
等)
这种优先级意味着在解析参数时,Spring 会先尝试用@RequestParam
的解析器处理,再尝试用@RequestBody
的解析器处理。
但实际上,由于@RequestParam
和@RequestBody
的适用场景不同(一个处理参数,一个处理请求体),它们很少会产生冲突。
六、常见面试题解析
@RequestParam
和@RequestBody
是 Java 面试中经常被问到的知识点,掌握以下常见问题的答案,能让你在面试中更有优势。
6.1 @RequestParam 和 @RequestBody 的区别是什么?
这是最基础也最常被问到的问题,回答时应涵盖数据位置、适用场景、支持的数据格式等方面:
@RequestParam
和@RequestBody
都是 SpringMVC 中用于获取请求数据的注解,主要区别如下:
- 数据位置不同:
@RequestParam
用于获取 URL 查询参数或表单参数,@RequestBody
用于获取请求体中的数据 - 适用 HTTP 方法:
@RequestParam
常用于 GET 方法,@RequestBody
常用于 POST、PUT 等方法 - 数据格式:
@RequestParam
处理键值对形式的数据,@RequestBody
处理 JSON、XML 等结构化数据 - 参数数量:
@RequestParam
可以同时获取多个参数,@RequestBody
通常一次处理一个对象 - 使用场景:简单参数用
@RequestParam
,复杂对象用@RequestBody
6.2 什么时候用 @RequestParam,什么时候用 @RequestBody?
回答这个问题时,应结合具体场景说明:
选择使用哪个注解主要取决于参数的类型、位置和复杂度:
- 当参数是简单类型(字符串、数字等)且位于 URL 查询字符串或表单中时,使用
@RequestParam
- 当参数是复杂对象(尤其是包含嵌套结构的对象)时,使用
@RequestBody
- GET 请求通常使用
@RequestParam
,因为 GET 请求的参数通常在 URL 中 - POST、PUT 等请求如果需要传递复杂数据,使用
@RequestBody
- 表单提交(
application/x-www-form-urlencoded
)使用@RequestParam
- JSON 或 XML 格式的数据使用
@RequestBody
简单来说,简单参数用@RequestParam
,复杂对象用@RequestBody
;URL 中的参数用@RequestParam
,请求体中的数据用@RequestBody
。
6.3 GET 请求可以用 @RequestBody 吗?
这个问题考察对 HTTP 规范和 SpringMVC 实现的理解:
从技术上讲,SpringMVC 允许在 GET 请求中使用@RequestBody
,但这是不推荐的做法,原因如下:
- HTTP 规范:GET 请求的主要目的是获取资源,通常不应该有请求体。虽然 HTTP 规范没有明确禁止 GET 请求有请求体,但很多服务器和代理对 GET 请求体的支持并不完善。
- 实际问题:
- 一些浏览器会忽略 GET 请求的请求体
- 缓存机制通常基于 URL,GET 请求体不会被纳入缓存键的计算
- 日志系统通常不会记录 GET 请求体,不利于问题排查
- 设计原则:RESTful 设计原则中,GET 请求用于查询,参数应该放在 URL 中,这也是
@RequestParam
的典型应用场景
因此,实际开发中应避免在 GET 请求中使用@RequestBody
,坚持使用@RequestParam
获取 GET 请求的参数。
6.4 @RequestParam 的 required 属性为 true 但参数不存在会怎样?
这个问题考察对异常处理的理解:
当@RequestParam
的required
属性为true
(默认值)但请求中没有对应的参数时,SpringMVC 会抛出MissingServletRequestParameterException
异常。
如果没有全局异常处理器处理这个异常,SpringMVC 会返回 400 Bad Request 响应给客户端。
为了提供更友好的错误信息,建议通过全局异常处理器捕获并处理这个异常:
java
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(MissingServletRequestParameterException.class)public String handleMissingParameter(MissingServletRequestParameterException e) {return "缺少必要参数:" + e.getParameterName();}
}
另外,可以通过设置required = false
并提供默认值来避免这种异常:
java
@RequestParam(required = false, defaultValue = "default") String param
6.5 @RequestBody 如何处理日期类型?
这个问题考察对实际开发中常见问题的处理能力:
@RequestBody
处理日期类型需要考虑 JSON 字符串到日期对象的转换,常用的解决方案有:
使用 @JsonFormat 注解:在实体类的日期字段上添加注解,指定日期格式
java
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;
配置全局日期格式:通过配置 Jackson 的 ObjectMapper,设置全局的日期序列化和反序列化格式
java
@Bean public ObjectMapper objectMapper() {ObjectMapper objectMapper = new ObjectMapper();JavaTimeModule module = new JavaTimeModule();module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));objectMapper.registerModule(module);return objectMapper; }
使用自定义序列化器 / 反序列化器:对于特殊的日期格式,可以编写自定义的序列化器和反序列化器
选择哪种方案取决于项目需求:单个字段的特殊格式用@JsonFormat
,统一的日期格式用全局配置。
七、总结与展望
@RequestParam
和@RequestBody
是 SpringBoot 开发中处理 HTTP 请求参数的核心注解,掌握它们的使用方法和底层原理,对于编写高质量的 Web 应用至关重要。