Spring MVC中400 Bad Request错误的常见来源与解决方案
作者:步行cgn
400 Bad Request 表示客户端请求有问题,服务器拒绝处理。在 Spring MVC 中,绝大多数 400 错误都来自参数绑定失败。把参数绑定的各个环节处理好,400 就能大幅减少。
一、400 错误的常见来源
| 来源 | 典型场景 | 异常 |
|---|---|---|
| 缺少必填参数 | @RequestParam 没传值 | MissingServletRequestParameterException |
| 参数类型不匹配 | ?page=abc 传给 int page | MethodArgumentTypeMismatchException |
| 请求体格式错误 | JSON 语法错误、字段类型不对 | HttpMessageNotReadableException |
| 参数校验失败 | @Valid 校验不通过 | MethodArgumentNotValidException |
| 路径变量类型错误 | /users/abc 传给 Long id | MethodArgumentTypeMismatchException |
| 缺少请求体 | POST 没带 body | HttpMessageNotReadableException |
| Content-Type 不匹配 | 接口要求 JSON,客户端发表单 | HttpMediaTypeNotSupportedException(415) |
二、针对每种来源的避免方法
1. 缺少必填参数
问题:
@GetMapping("/list")
public List<User> list(@RequestParam String name) { }
访问 /list 不带 name,直接 400。
解决:
可选参数设置 required = false 或 defaultValue:
@GetMapping("/list")
public List<User> list(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) { }
基本类型一定要给 defaultValue,否则缺少参数时无法赋值给 int,仍然会 400。
2. 参数类型不匹配
问题:
@GetMapping("/list")
public List<User> list(@RequestParam int page) { }
访问 /list?page=abc,字符串转 int 失败,400。
解决:
- 前端保证参数格式正确。
- 后端用
String接收后手动转换,或使用全局异常处理返回友好提示。 - 对于日期、枚举等复杂类型,提供自定义
Converter或@DateTimeFormat。
@GetMapping("/list")
public List<User> list(
@RequestParam(defaultValue = "1")
@Min(value = 1, message = "页码不能小于1") int page) { }
3. 请求体格式错误
问题:
@PostMapping("/user")
public User create(@RequestBody User user) { }
客户端发送的 JSON 语法错误,或字段类型不匹配(比如 age 传了字符串),抛出 HttpMessageNotReadableException,400。
解决:
- 前端保证 JSON 格式正确。
- 后端用全局异常处理捕获
HttpMessageNotReadableException,返回明确的错误信息。 - 避免用基本类型接收可能为空的字段,用包装类型。
public class User {
private Long id;
private String name;
private Integer age; // 用 Integer 而不是 int,允许 null
}
4. 参数校验失败
问题:
@PostMapping("/user")
public User create(@RequestBody @Valid User user) { }
User 的 name 标注了 @NotBlank,但请求体没传 name,抛出 MethodArgumentNotValidException,默认返回 400。
解决:
校验失败返回 400 是合理的,但可以自定义错误响应,让客户端知道具体哪个字段不合法。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.toList());
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "参数校验失败", errors));
}
}
如果希望校验失败返回 422 而不是 400,可以用 @ResponseStatus 自定义。
5. 路径变量类型错误
问题:
@GetMapping("/users/{id}")
public User getById(@PathVariable Long id) { }
访问 /users/abc,字符串转 Long 失败,400。
解决:
用正则约束路径变量:
@GetMapping("/users/{id:\\d+}")
public User getById(@PathVariable Long id) { }
abc 不匹配 \d+,请求不会进入这个方法,而是返回 404,语义更准确。
6. 缺少请求体
问题:
@PostMapping("/user")
public User create(@RequestBody User user) { }
客户端 POST 但没有请求体,抛出 HttpMessageNotReadableException。
解决:
- 前端确保 POST/PUT 请求携带请求体。
- 如果请求体可选,设置
@RequestBody(required = false),并在方法内判空。
@PostMapping("/user")
public User create(@RequestBody(required = false) User user) {
if (user == null) {
throw new BusinessException("请求体不能为空");
}
return userService.save(user);
}
7. Content-Type 不匹配
问题:
@PostMapping(value = "/user", consumes = "application/json")
public User create(@RequestBody User user) { }
客户端发送 Content-Type: application/x-www-form-urlencoded,返回 415(Unsupported Media Type),不是 400,但同样是客户端请求问题。
解决:
- 前端发送 JSON 时设置
Content-Type: application/json。 - 接口不要过度限制
consumes,除非确实需要。
三、全局异常处理兜底
即使前端尽力了,400 仍然可能发生。用 @RestControllerAdvice 统一捕获参数绑定异常,返回结构化错误信息,避免暴露堆栈。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 缺少必填参数
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ErrorResponse> handleMissingParam(
MissingServletRequestParameterException e) {
String message = "缺少必填参数:" + e.getParameterName();
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, message, null));
}
// 参数类型不匹配
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(
MethodArgumentTypeMismatchException e) {
String message = String.format("参数 %s 类型错误,期望类型:%s",
e.getName(), e.getRequiredType().getSimpleName());
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, message, null));
}
// 请求体不可读(JSON 格式错误、字段类型错误、缺少请求体)
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> handleNotReadable(
HttpMessageNotReadableException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "请求体格式错误", null));
}
// 参数校验失败
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.toList());
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "参数校验失败", errors));
}
// 兜底
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("未处理异常", e);
return ResponseEntity.status(500)
.body(new ErrorResponse(500, "服务器内部错误", null));
}
}
统一响应结构:
@Data
@AllArgsConstructor
public class ErrorResponse {
private int code;
private String message;
private List<String> errors;
}
四、参数绑定层面的预防措施
1. 能用包装类型就不用基本类型
// ❌ 基本类型,缺少参数时无法赋值,直接 400 @RequestParam int page // ✅ 包装类型,缺少时为 null,可以给默认值或判空 @RequestParam Integer page
2. 给可选参数明确默认值
@RequestParam(defaultValue = "1") int page @RequestParam(defaultValue = "10") int size @RequestParam(defaultValue = "createTime,desc") String sort
3. 路径变量加正则约束
@GetMapping("/users/{id:\\d+}")
public User getById(@PathVariable Long id) { }
非法 ID 直接 404,不会进入方法,也不会触发类型转换异常。
4. 复杂类型提供自定义转换器
@Component
public class LocalDateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String source) {
try {
return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} catch (Exception e) {
throw new IllegalArgumentException("日期格式错误,应为 yyyy-MM-dd");
}
}
}
5. 接口契约明确
在 Swagger/OpenAPI 文档中明确每个参数是否必填、类型、格式、取值范围。前端按契约传参,从源头减少 400。
五、前端层面的配合
400 错误虽然由后端返回,但根因往往是前端传参不对。前后端约定好:
- 必填字段前端校验,不传空值。
- 参数类型与接口契约一致,数字不要传字符串。
- POST/PUT 请求设置正确的
Content-Type。 - JSON 序列化用标准库,避免手工拼接导致格式错误。
- 对用户输入做格式校验,比如日期、邮箱、手机号。
六、总结
| 400 来源 | 避免方法 |
|---|---|
| 缺少必填参数 | required = false 或 defaultValue |
| 参数类型不匹配 | 前端保证格式,后端用包装类型,全局异常处理 |
| JSON 格式错误 | 前端用标准序列化,后端捕获 HttpMessageNotReadableException |
| 参数校验失败 | @Validated + 全局异常处理返回字段级错误 |
| 路径变量类型错误 | 正则约束 {id:\\d+} |
| 缺少请求体 | @RequestBody(required = false) 判空 |
| Content-Type 不匹配 | 前端设置正确的 Content-Type |
避免 400 的核心思路是:参数可选性明确、类型安全、格式校验前移、全局异常兜底。后端不要依赖默认行为,给可选参数设默认值,用包装类型代替基本类型,路径变量加正则。同时用 @RestControllerAdvice 统一捕获参数绑定异常,返回结构化错误信息,让客户端知道错在哪里。400 不可能完全消灭,但可以做到“发生时有明确提示,而不是一堆堆栈”。
以上就是Spring MVC中400 Bad Request错误的常见来源与解决方案的详细内容,更多关于Spring MVC 400 Bad Request错误的资料请关注脚本之家其它相关文章!
