主题
SpringMVC 参数绑定、@Valid 校验与文件上传
提出问题
接口参数怎么进 Controller 的,很多人用了三年 Spring 也说不清。@RequestParam 和 @RequestBody 虽然眼熟,但要问它们各自走了哪个解析器、@Valid 校验失败后异常被谁捕获、文件上传的 MultipartFile 上限在哪配——大部分人只能答个大概。
生产上常见的坑:@RequestParam 中文变乱码,前端传的 JSON 里日期格式解析失败,Long 转 JSON 给前端丢精度,上传文件超过 1MB 直接 500 不提示。这些问题不是 Spring 的 bug,是对参数绑定机制的理解不够。
理解参数绑定的核心是 HandlerMethodArgumentResolver 接口——SpringMVC 用一系列解析器把 HTTP 请求内容转换成方法参数。每个参数类型和注解组合对应一个解析器,走完解析器列表找到匹配的就执行,找不到就报 400。
分析问题
参数绑定与 HandlerMethodArgumentResolver
先看 @RequestParam 和 @RequestBody 的区别。@RequestParam 走 RequestParamMethodArgumentResolver,从 HttpServletRequest.getParameterMap() 里取键值对,适合 GET 请求的 query string 或 POST 表单的 application/x-www-form-urlencoded 数据。@RequestBody 走 RequestResponseBodyMethodProcessor,它依赖 HttpMessageConverter 把请求体(InputStream)反序列化成 Java 对象。
java
@RestController
public class UserController {
// 走 RequestParamMethodArgumentResolver
@GetMapping("/user")
public User getUser(@RequestParam Long id,
@RequestParam(defaultValue = "zh") String lang) {
return userService.findById(id);
}
// 走 RequestResponseBodyMethodProcessor + HttpMessageConverter
@PostMapping("/user")
public User createUser(@RequestBody @Valid UserCreateRequest req) {
return userService.create(req);
}
}常见坑:@RequestParam 遇到中文时,Tomcat 的默认解码是 ISO-8859-1,只要 URL 里带了中文就会乱码。解法是在 server.tomcat.uri-encoding=UTF-8(Spring Boot 默认已配,但如果你手动配了 Connector 就容易被覆盖)。或者前端用 encodeURIComponent 编码后再传。
@RequestBody 的反序列化依赖 Jackson 的 ObjectMapper。三个高频坑:
日期格式:前端传
"2026-09-01 10:00:00",Jackson 默认不认识不带 T 的格式,报InvalidFormatException。需要在application.yml配spring.jackson.date-format=yyyy-MM-dd HH:mm:ss或直接在字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。Long 精度丢失:Java 的 Long 有 19 位,JS 的 Number 安全精度只有 53 位(约 16 位十进制)。前端拿到 1743158400000 这种 13 位时间戳还好,但 ID 如果超过 16 位,前端会丢失精度。配
@JsonSerialize(using = ToStringSerializer.class)把 Long 转成字符串发出去。泛型擦除:
@RequestBody List<Long> ids没问题,但如果是@RequestBody Result<List<Long>> data,Jackson 在反序列化时不知道List<>里的泛型是 Long,会反成List<LinkedHashMap>。需要TypeReference或自定义HttpMessageConverter。
Bean Validation:@Valid vs @Validated
@Valid 是 JSR-380 标准注解,@Validated 是 Spring 的增强版。核心区别在分组校验。
java
@Data
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@Email
private String email;
@NotNull(groups = AdminCreate.class)
private String role;
@Valid // 嵌套校验
private Address address;
public interface AdminCreate {}
}
@RestController
public class UserController {
@PostMapping("/user")
public User create(@RequestBody @Validated UserCreateRequest req) {
return userService.create(req);
}
@PostMapping("/admin/user")
public User createAdmin(@RequestBody @Validated(UserCreateRequest.AdminCreate.class)
UserCreateRequest req) {
return userService.createAsAdmin(req);
}
}@Valid 不支持分组,@Validated 支持。但 @Validated 不能用在字段上做嵌套校验的触发——嵌套校验仍然是 @Valid 的职责。所以常见写法是 @Validated 在方法参数上激活分组,实体字段上用 @Valid 触发嵌套校验。
校验失败后,框架会在进入 Controller 方法体之前抛出异常:@RequestBody 校验失败走 MethodArgumentNotValidException(Spring 封装),@RequestParam 校验失败走 ConstraintViolationException。全局异常处理用 @ControllerAdvice 捕获这两类:
java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleValidation(
ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations()
.forEach(v -> errors.put(v.getPropertyPath().toString(), v.getMessage()));
return ResponseEntity.badRequest().body(errors);
}
}文件上传
文件上传走 MultipartResolver,Spring Boot 自动配置了 StandardServletMultipartResolver。参数用 MultipartFile 声明:
java
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "请选择文件";
}
// 存到本地或 OSS
String originalFilename = file.getOriginalFilename();
String contentType = file.getContentType();
long size = file.getSize();
// 务必对文件名做防注入处理,避免 ../ 路径穿越
String safeName = UUID.randomUUID() + "_" +
originalFilename.replaceAll("[^a-zA-Z0-9.\\-_]", "_");
file.transferTo(new File("/data/upload/" + safeName));
return "上传成功";
}配置项在 application.yml:
yaml
spring:
servlet:
multipart:
max-file-size: 10MB # 单文件上限
max-request-size: 50MB # 一次请求总大小
location: /tmp/upload # 临时目录,默认 /tmp/servlet-upload-xxx四个坑:
上传超时:大文件配合 nginx 时,nginx 默认
proxy_read_timeout60s,上传慢会被 nginx 切断。需要调大超时时间。临时目录被清理:默认
/tmp/servlet-upload-xxx的临时目录在 Linux 上可能被 systemd 的 tmpfiles.d 清理掉。显式指定location避免生产环境出这种偶发 500。文件名注入:
originalFilename可能带../路径穿越,需要做安全过滤——只保留字母数字和点、下划线、中划线,用 UUID 重命名更安全。内存溢出:
MultipartFile默认只在内存 < 256KB 时存内存,超过会写磁盘临时文件,但极端情况下并发上传大文件还是可能撑爆内存,配合连接池限流。
总结
参数绑定这条链从请求进来到业务方法拿到参数,涉及三层:HandlerMethodArgumentResolver 选型决定参数来源 → HttpMessageConverter 处理序列化 → Bean Validation 做校验拦截。三层里任何一层没配好,前端就收到 400 或 500。
面试一个常见追问链条:讲一下 @RequestBody 和 @RequestParam 区别 → 怎么配校验 → 校验失败返回什么异常 → 怎么自定义错误消息 → 文件上传超时怎么排查。按这个链过一遍,基本把这整块说透了。
参考:Spring Framework 源码
org.springframework.web.method.support.HandlerMethodArgumentResolver/org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor/ Spring Boot 官方文档 Core Features 章节 Validation 和 File Upload 部分。