易君召
发布于 2026-08-13 / 作者:易君召 / 3 阅读
0

API 开发 POST 请求数据验证最佳实践

POST 请求大多接收 JSON / Form / Multipart 表单数据,数据验证核心目标:拦截非法输入、保证业务合法性、返回友好错误、避免后续逻辑报错。下面从通用思路、分层校验、Java (SpringBoot) 示例、常见坑、错误响应规范完整说明。

一、POST 常见数据来源

  1. application/json:请求体 JSON(最常用)

  2. application/x‑www‑form‑urlencoded:表单编码

  3. multipart/form‑data:文件上传 + 表单字段

注意:SpringBoot 中 @RequestBody 读取 JSON;@ModelAttribute / @RequestParam 读取表单;文件用 @RequestPart

二、验证分层思想(重要)

❌ 不要全部写在 Controller 的 if‑else,维护灾难。

层级

职责

1. 框架层 (JSR‑380 Bean Validation)

格式校验:非空、长度、手机号、邮箱、数值范围、正则

2. 业务层校验

业务规则:账号是否存在、状态是否允许操作、金额不能小于账户余额、唯一约束

3. 数据库层

兜底:唯一索引、非空约束,防止绕过应用层直接写入脏数据

原则:格式校验放 DTO,业务校验放 Service,数据库做兜底防护

三、SpringBoot 完整实战(Java17 + SpringBoot3)

1. 引入依赖

SpringBoot3 使用 jakarta.validation

xml

<!-- validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2. 定义 POST 请求 DTO,加上校验注解

java

运行

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class UserCreateDTO {
    // 不为null,字符串不能全空格
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度2‑20位")
    private String username;

    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
    private String phone;

    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄不能小于1")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;

    @Email(message = "邮箱格式不正确")
    private String email;
}

常用注解:

  • @NotNull:不能为 null(允许空字符串)

  • @NotBlank:字符串不能 null、不能空白串

  • @NotEmpty:集合 / 字符串不能 null 且长度 > 0

  • @Size:字符串、集合长度

  • @Min/@Max:数字大小

  • @Pattern:自定义正则

3.Controller 开启校验:@Valid

java

运行

import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/user")
public class UserController {

    @PostMapping("/create")
    public R<?> createUser(@Valid @RequestBody UserCreateDTO dto){
        // @Valid 触发DTO校验,校验失败抛出 MethodArgumentNotValidException
        userService.create(dto);
        return R.success();
    }
}

区别:

  • @Valid:jakarta,触发嵌套对象校验;

  • @Validated:spring,支持分组校验,可写在类上。

4. 全局异常处理器统一捕获校验异常

不要把原生异常堆栈抛给前端,统一返回标准错误 JSON。

java

运行

@RestControllerAdvice
public class GlobalExceptionHandler {

    // JSON请求体校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<?> handleValidException(MethodArgumentNotValidException e){
        Map<String,String> errMap = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(err->{
            errMap.put(err.getField(), err.getDefaultMessage());
        });
        return R.fail(400, "参数校验失败", errMap);
    }

    // Form表单校验异常
    @ExceptionHandler(BindException.class)
    public R<?> handleBindException(BindException e){
        Map<String,String> errMap = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(err->{
            errMap.put(err.getField(), err.getDefaultMessage());
        });
        return R.fail(400, "表单参数错误", errMap);
    }
}

返回示例:

json

{
  "code":400,
  "msg":"参数校验失败",
  "data":{
    "username":"用户名不能为空",
    "phone":"手机号格式错误"
  }
}

5. 分组校验(场景:新增 / 更新复用同一个 DTO)

同一个 DTO,新增 id 不能为空;更新 id 必须不为空。

java

运行

// 分组标记接口
public interface Create {}
public interface Update {}

@Data
public class UserDTO{
    @Null(groups = Create.class, message = "新增时不能传id")
    @NotNull(groups = Update.class, message = "更新必须传入id")
    private Long id;

    @NotBlank(groups = {Create.class, Update.class}, message = "用户名不能为空")
    private String username;
}

Controller 使用@Validated指定分组:

java

运行

@PostMapping("/add")
public R<?> add(@Validated(Create.class) @RequestBody UserDTO dto){...}

@PostMapping("/update")
public R<?> update(@Validated(Update.class) @RequestBody UserDTO dto){...}

6. 自定义校验注解(复杂业务格式,比如身份证)

实现ConstraintValidator,自定义注解,适合通用格式规则。

7.Service 层业务校验

Bean Validation 只做格式;业务逻辑校验写在 Service。

java

运行

@Service
public class UserService {
    public void create(UserCreateDTO dto){
        // 业务校验:手机号是否已注册
        if(userMapper.existsByPhone(dto.getPhone())){
            throw new BusinessException(400,"该手机号已注册");
        }
        // 其他业务规则……
    }
}

四、Multipart/form‑data 文件上传 POST 校验

文件大小、格式校验:

  1. application.yml 配置全局文件限制

yaml

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 20MB
  1. DTO 搭配@Validated校验文件对象,判断文件后缀、mime 类型。

五、通用 API 校验规范(所有语言通用)

  1. HTTP 状态码

    • 参数非法:400 Bad Request,不要返回 200;

    • 认证失败:401;无权限 403;

  2. 返回格式:把字段名 + 错误信息返回,前端可以直接对应表单提示;

  3. 拒绝超大输入:限制字符串最大长度,防止大 JSON 攻击;

  4. 防止空对象:POST 请求体不能为 null,@RequestBody(required=true)

  5. 数值校验:防止负数、超大数字,避免数据库溢出;

  6. 集合校验:校验集合内每一项(开启嵌套校验,字段上加@Valid

嵌套示例:

java

运行

@Data
public class OrderDTO{
    @NotEmpty(message="订单明细不能为空")
    @Valid // 触发集合内部每一个ItemDTO校验
    private List<OrderItemDTO> itemList;
}

六、常见踩坑点

  1. @NotNull用于字符串,空字符串""会放行,应该用@NotBlank

  2. ❌ 忘记写@Valid / @Validated,注解完全不生效;

  3. ❌ DTO 内部集合对象,不加@Valid,集合内部字段校验失效;

  4. ❌ 业务校验写在 Controller,代码臃肿,无法复用;

  5. ❌ 直接返回原生校验异常栈给前端,泄露内部信息;

  6. ❌ 只做应用层校验,数据库不加非空、唯一索引,并发场景产生脏数据。

七、其他语言简单参考

  • Node.js(Express):joi / zod

  • Python(Django/FastAPI):Pydantic;

  • Go:go‑playground/validator。

核心思想一致:入参先校验,再执行业务;格式校验用框架,业务校验手写,全局统一错误输出。


本文原创作者:易君召,详见:https://www.yijunzhao.cn/authors/yijunzhao,转载请注明出处。

原文链接 https://www.yijunzhao.cn/archives/api-post-request-data-validation-best-practices

欢迎访问 小易撩挨踢

https://www.yijunzhao.cn/