java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring MVC 400 Bad Request错误

Spring MVC中400 Bad Request错误的常见来源与解决方案

作者:步行cgn

本文系统分析Spring MVC中400 Bad Request错误的七大常见来源,包括缺少必填参数、类型不匹配、请求体格式错误、校验失败、路径变量类型错误、缺失请求体及Content-Type不匹配,需要的朋友可以参考下

400 Bad Request 表示客户端请求有问题,服务器拒绝处理。在 Spring MVC 中,绝大多数 400 错误都来自参数绑定失败。把参数绑定的各个环节处理好,400 就能大幅减少。

一、400 错误的常见来源

来源典型场景异常
缺少必填参数@RequestParam 没传值MissingServletRequestParameterException
参数类型不匹配?page=abc 传给 int pageMethodArgumentTypeMismatchException
请求体格式错误JSON 语法错误、字段类型不对HttpMessageNotReadableException
参数校验失败@Valid 校验不通过MethodArgumentNotValidException
路径变量类型错误/users/abc 传给 Long idMethodArgumentTypeMismatchException
缺少请求体POST 没带 bodyHttpMessageNotReadableException
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。

解决:

@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。

解决:

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。

解决:

@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,但同样是客户端请求问题。

解决:

三、全局异常处理兜底

即使前端尽力了,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 错误虽然由后端返回,但根因往往是前端传参不对。前后端约定好:

六、总结

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错误的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文