springboot整合validation详细教程

1.快速集成

A 添加依赖

在 Spring Boot 3.x (2026年主流版本) 中,spring-boot-starter-validation 是进行参数校验的标准且唯一推荐 的方式。它基于 Jakarta Bean Validation 3.0 (原 JSR-380) 规范,底层默认使用 Hibernate Validator 实现。

在 Spring Boot 2.3 之前,该依赖包含在 spring-boot-starter-web 中。但从 2.3 开始(包括现在的 3.x/4.x),必须显式添加

Maven (pom.xml):

xml 复制代码
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-validation</artifactId> 
</dependency>

2. 核心用法:三步走

第一步:在 DTO/VO 类上定义规则

使用注解标记字段的约束条件。

java 复制代码
package com.example.demo.dto;

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

@Data
public class UserRegisterDTO {

    @NotNull(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
    private String username;

    @NotBlank(message = "密码不能为空") // 专门用于字符串,检查 null, "", "   "
    @Pattern(regexp = "^[a-zA-Z0-9]{6,20}$", message = "密码必须是6-20位字母或数字")
    private String password;

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

    @Min(value = 18, message = "必须年满18岁")
    @Max(value = 100, message = "年龄不能超过100岁")
    private Integer age;

    @Past(message = "出生日期必须是过去的时间")
    private java.time.LocalDate birthday;

    // 嵌套对象校验 (见下文高级篇)
    @Valid
    private AddressDTO address;
}

第二步:在 Controller 中启用校验

在参数前添加 @Valid (或 Spring 特有的 @Validated)。

java 复制代码
import jakarta.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    /**
     * 方式一:配合 BindingResult 手动处理错误
     * 如果校验失败,errors.hasErrors() 为 true
     */
    @PostMapping("/register-v1")
    public ResponseEntity<?> registerV1(@Valid @RequestBody UserRegisterDTO dto,
                                        BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            // 手动提取错误信息
            StringBuilder errorMsg = new StringBuilder();
            bindingResult.getFieldErrors().forEach(err ->
                    errorMsg.append(err.getField()).append(":").append(err.getDefaultMessage()).append("; ")
            );
            return ResponseEntity.badRequest().body(errorMsg.toString());
        }

        // 业务逻辑...
        return ResponseEntity.ok("注册成功");
    }

    /**
     * 方式二:推荐!配合全局异常处理器 (见第三步)
     * 不需要 BindingResult,校验失败直接抛异常
     */
    @PostMapping("/register-v2")
    public ResponseEntity<?> registerV2(@Valid @RequestBody UserRegisterDTO dto) {
        // 如果代码能执行到这里,说明校验已通过
        return ResponseEntity.ok("注册成功 (v2)");
    }
}

第三步:全局统一异常处理 (最佳实践)

不要让每个 Controller 都写 if (bindingResult.hasErrors())。创建一个全局Advice来捕获 MethodArgumentNotValidException

java 复制代码
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalValidationExceptionHandler {

    /**
     * 捕获 @Valid 校验失败的异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {

        Map<String, String> errors = new HashMap<>();

        // 遍历所有字段错误
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        // 返回格式:{ "username": "用户名不能为空", "password": "密码必须是..." }
        return ResponseEntity.badRequest().body(errors);
    }

    /**
     * 捕获 @RequestParam / @PathVariable 校验失败的异常 (需配合 @Validated)
     */
    @ExceptionHandler(org.springframework.validation.ConstraintViolationException.class)
    public ResponseEntity<Map<String, String>> handleConstraintViolationException(
            org.springframework.validation.ConstraintViolationException ex) {

        Map<String, String> errors = new HashMap<>();
        ex.getConstraintViolations().forEach(violation -> {
            String path = violation.getPropertyPath().toString();
            errors.put(path, violation.getMessage());
        });

        return ResponseEntity.badRequest().body(errors);
    }
}

3. 常用注解速查表 (Jakarta 版)

注解 适用类型 说明
@NotNull 任何对象 不能为 null,但可以是空字符串 ""
@NotBlank String 不能为 null,不能为空字符串 "",也不能全是空格 " "。字符串首选。
@NotEmpty 集合/数组/String 不能为 null,且大小/长度必须 > 0。
@Size String/Collection/Array 限制长度或大小 (min, max)。
@Min / @Max 数值类型 限制数值范围。
@DecimalMin BigDecimal 限制小数范围。
@Email String 校验邮箱格式。
@Pattern String 正则表达式校验。
@Past / @Future Date/LocalDate 时间必须是过去或未来。
@Positive 数值类型 必须是正数 (>0)。
@Valid 对象/集合 级联校验。如果字段是对象或 List<Object),加上此注解会递归校验内部字段。

4. 高级功能

A. 分组校验 (Group Validation)

场景:同一个 DTO,新增 时 ID 应为 null,修改时 ID 必填。

  1. 定义分组接口
java 复制代码
public interface CreateGroup {}
public interface UpdateGroup {}
  1. 在 DTO 中指定分组
java 复制代码
public class UserDTO {
    // 仅在更新时需要校验
    @NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
    private Long id;

    // 仅在创建时需要校验
    @NotBlank(groups = CreateGroup.class, message = "创建时用户名必填")
    private String username;
}
  1. 在 Controller 中指定分组
java 复制代码
// 创建接口:只校验 CreateGroup
@PostMapping
public String create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { ... }

// 更新接口:只校验 UpdateGroup
@PutMapping
public String update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) { ... }

B. 自定义校验注解

场景:校验手机号、身份证、或特定的业务规则(如"结束时间必须大于开始时间")。

  1. 创建注解
java 复制代码
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class) // 指定验证器
public @interface IsValidPhone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. 实现验证器逻辑
java 复制代码
public class PhoneValidator implements ConstraintValidator<IsValidPhone, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true; // null 由 @NotNull 处理
        return value.matches("^1[3-9]\d{9}$"); // 简单正则
    }
}
  1. 使用
java 复制代码
@IsValidPhone(message = "请输入正确的中国大陆手机号")
private String phone;

C. 跨字段校验 (类级别注解)

场景:endTime 必须大于 startTime

java 复制代码
@Data
@AssertTrue(message = "结束时间必须晚于开始时间", property = "endTime") // 这种写法较难定位具体字段,通常用类级别约束
public class EventDTO {
    private LocalDateTime startTime;
    private LocalDateTime endTime;

    // 更好的方式:自定义类级别注解
}
// 或者直接在类上加逻辑判断,或在自定义注解中获取整个对象

更优雅的做法是自定义一个类级别注解 @Constraint(validatedBy = EventValidator.class),在 Validator 中拿到整个 EventDTO 对象进行比较。

5. 常见坑与注意事项

  1. @Valid vs @Validated:

    • @Valid (Jakarta 标准): 支持嵌套校验,不支持分组
    • @Validated (Spring 扩展): 支持分组校验,支持在类级别开启校验。
    • 建议 :Controller 参数优先用 @Valid (兼容性好);如果需要分组,必须用 @Validated
  2. 嵌套对象失效

    • 如果 DTO 中包含另一个对象(如 private Address address;),必须在字段前加 @Valid,否则内部字段不会校验。
    • 如果是 List<Address>,同样需要 @Valid
  3. 基本数据类型 vs 包装类型

    • int age:默认值是 0,@Min(1) 可能会失效(因为 0 也是值),或者无法区分"未传参"和"传了0"。
    • 建议 :校验参数尽量使用包装类型 (Integer, Long),配合 @NotNull 使用。
  4. 性能问题

    • 正则表达式 (@Pattern) 复杂时会消耗 CPU,尽量避免在高频接口使用极其复杂的正则。

6.这是一份针对 Spring Boot 3.x / Jakarta Bean Validation 3.0 的注解速查表。

⚠️ 重要提示

  • Spring Boot 3+ 必须使用 jakarta.validation.constraints.* 包。
  • 旧版 Spring Boot 2.x 使用 javax.validation.constraints.*
  • 不要混用,否则校验会直接失效且不报错!

1. 空值与字符串检查 (最常用)

表格

注解 适用类型 规则说明 典型场景
@NotNull 任何对象 不能为 null。 ✅ 允许 "", " ", 0 非字符串对象、包装类型 (Integer, Date)
@NotBlank String 不能为 null,且去除空格后长度 > 0。 ❌ 拒绝 null, "", " " 用户名、密码、标题 (字符串首选)
@NotEmpty String, Collection, Map, Array 不能为 null,且长度/大小 > 0。 ❌ 拒绝 null, "", [] 列表参数、非空字符串
@Null 任何对象 必须为 null 某些特定状态字段

2. 数值大小检查

表格

注解 适用类型 规则说明 属性示例
@Min(value) 数值类型 (int, long, BigDecimal等) 值必须 value @Min(18) (年龄最小18)
@Max(value) 数值类型 值必须 value @Max(100) (分数最大100)
@DecimalMin(value) BigDecimal, String 值必须 value (支持小数) @DecimalMin("0.01")
@DecimalMax(value) BigDecimal, String 值必须 value (支持小数) @DecimalMax("99.99")
@Positive 数值类型 必须 > 0 (正数) 价格、数量
@PositiveOrZero 数值类型 必须 ≥ 0 (非负数) 余额、积分
@Negative 数值类型 必须 < 0 (负数) 亏损额
@NegativeOrZero 数值类型 必须 ≤ 0 (非正数) 抵扣额

3. 格式与模式检查

表格

注解 适用类型 规则说明 属性示例
@Email String 必须是合法的邮箱格式 @Email(message="邮箱不对")
@Pattern(regexp) String 必须符合正则表达式 @Pattern(regexp="^1[3-9]\d{9}$") (手机号)
@Size(min, max) String, Collection, Array 长度/大小必须在范围内 @Size(min=6, max=20) (密码长度)
@Digits(integer, fraction) 数值类型 整数位和小数位限制 @Digits(integer=5, fraction=2) (金额)

4. 日期与时间检查

表格

注解 适用类型 规则说明
@Past Date, LocalDate, LocalDateTime 必须是过去的时间 (昨天及以前)
@PastOrPresent 日期类型 必须是过去或现在 (不能是未来)
@Future 日期类型 必须是未来的时间 (明天及以后)
@FutureOrPresent 日期类型 必须是未来或现在 (不能是过去)

5. 布尔与逻辑检查

表格

注解 适用类型 规则说明
@AssertTrue boolean, Boolean 必须为 true
@AssertFalse boolean, Boolean 必须为 false

6. 级联与分组 (高级控制)

表格

注解 作用域 说明
@Valid 字段 (对象/集合) 级联校验 。如果字段是对象或 List<Object>,加上此注解会递归校验内部属性。非常重要,常漏加!
@Validated 类/方法参数 Spring 提供的扩展注解。 1. 支持分组校验 (@Validated(Group.class)) 2. 支持在 Controller 类级别开启校验

💡 避坑指南 & 最佳实践

1. @NotNull vs @NotBlank vs @NotEmpty

这是面试和开发中最容易混淆的:

  • 对象/数字 :只用 @NotNull

  • 字符串 :优先用 @NotBlank (它涵盖了 null 和 空串/空格)。

    • ❌ 错误:@NotNull + @NotEmpty (重复且 @NotEmpty 不检查空格)
    • ✅ 正确:@NotBlank
  • 集合/数组 :用 @NotEmpty (集合没有"空白"概念,只有空)。

2. 基本类型 vs 包装类型

  • 基本类型 (int, double) :默认有初始值 (0, 0.0), @NotNull 对它们无效 (因为它们永远不为 null)。
  • 建议 :DTO 中的校验字段全部使用 包装类型 (Integer, Double, Long),这样 @NotNull 才能生效,区分"未传参"和"传了0"。

3. 嵌套对象必须加 @Valid

如果不加 @Valid,内部的 Address 对象即使填了非法数据,也不会报错。

java 复制代码
public class UserDTO {
    @NotNull
    private String name;

    // ❌ 错误写法:address 内部的字段不会校验
    private Address address;

    // ✅ 正确写法:开启级联校验
    @Valid
    private Address address;
}

4. 自定义错误消息

所有注解都支持 message 属性,支持占位符:

java 复制代码
@Size(min = 2, max = 10, message = "长度必须在 {min} 到 {max} 之间,当前长度为 {validatedValue}")

5. 常用组合模板

  • 用户名 : @NotBlank @Size(min=2, max=20)
  • 密码 : @NotBlank @Pattern(regexp="...")
  • 年龄 : @NotNull @Min(1) @Max(150)
  • 邮箱 : @NotBlank @Email
  • 手机号 : @NotBlank @Pattern(regexp="^1[3-9]\d{9}$")
  • 状态枚举 : @NotNull (配合后端逻辑判断)
  • 开始/结束时间 : @NotNull + 自定义类级别注解 (比较两个时间)

总结流程图

graph TD A[前端请求 JSON] --> B("Controller @Valid") B --> C{参数校验} C -- 失败 --> D[抛出 MethodArgumentNotValidException] D --> E["全局异常处理器 @RestControllerAdvice"] E --> F[返回统一格式 400 Error JSON] C -- 成功 --> G[执行业务逻辑]

掌握这套流程,你的 Spring Boot 接口将具备强大的防御能力,杜绝脏数据进入业务层。

相关推荐
神奇小汤圆1 小时前
为什么redis不能跨机房部署哨兵模式
后端
锦木烁光1 小时前
Flowable 实战:从架构解耦到多状态动态查询的高性能重构方案
前端·后端
None3212 小时前
【NestJs】Websocket 通关指南:从入门到实战
后端·node.js
yuyu_03042 小时前
Spring Boot在Windows开机自启动
windows·spring boot·后端
L0CK2 小时前
高级篇 05. 多级缓存 - JVM 进程缓存之实现业务缓存
后端
Oneslide2 小时前
基于Nginx实现目录列表展示与文件下载服务(K8s ConfigMap配置版)
后端
Java编程爱好者2 小时前
对于java工程师(高级)的面试如果只考3道题,就能看出他的真实水平
后端
PFinal社区_南丞2 小时前
Go-1.26-五年最差版本-Bug 深度分析
后端
三水不滴2 小时前
Elasticsearch 实战系列(一):从核心基础概念入门到实战落地
后端·elasticsearch