验证用户输入以维护数据完整性是应用程序逻辑的重要组成部分。即使是最简单的应用程序,数据验证也是必不可少的。

Jakarta Bean Validation 模型通过对 JavaBeans 组件(例如托管 bean)的字段、方法或类进行注解形式的约束来支持。

入门使用

1.检查是否有相应依赖,如果没有则引入

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

2.DTO添加校验注解

@NotBlank(message = "手机号不能为空")
private String phone;

3.Controller添加注解@Validated使其生效

@PostMapping
@ApiOperation("新增员工")
public Result save(@Validated @RequestBody EmployeeDTO employeeDTO){
    employeeService.save(employeeDTO);
    return Result.success();
}

// JSON 请求体 → MethodArgumentNotValidException
@PostMapping("/user/json")
public Result createByJson(@Validated @RequestBody User user) {
    return Result.success();
}

// 表单提交 → BindException  
@PostMapping("/user/form")
public Result createByForm(@Validated User user) {  // 没有 @RequestBody
    return Result.success();
}

4.接收校验错误结果,参数在校验失败的时候会抛出的MethodArgumentNotValidException或者BindException两种异常,可以在全局的异常处理器中捕捉到这两种异常 第三种异常不常见 通过aop触发

/**
 * 自定义验证异常
 */
//处理表单/Query参数绑定到对象时的校验失败
@ExceptionHandler(BindException.class)
public Result validatedBindException(BindException e)
{
    log.error(e.getMessage(), e);
    String message = e.getAllErrors().get(0).getDefaultMessage();
    return Result.error(message);
}

/**
 * 自定义验证异常
 */
//处理 JSON 请求体绑定到对象时的校验失败
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object validExceptionHandler(MethodArgumentNotValidException e)
{
    log.error(e.getMessage(), e);
    // getFieldError()不指定参数,会默认取第一个错误信息。
    String message = e.getBindingResult().getFieldError().getDefaultMessage();
    return Result.error(message);
}

//Service或其他地方上启用了方法校验  通过Spring AOP 的方法校验异常
@ExceptionHandler(ConstraintViolationException.class)
public Result constraintViolationException(
        ConstraintViolationException e) {

    log.error(e.getMessage(), e);

    String message = e.getConstraintViolations()
            .stream()
            .findFirst()
            .map(ConstraintViolation::getMessage)
            .orElse("参数校验失败");

    return Result.error(message);
}

内置注解约束

约束描述例子
@AssertFalse字段或属性的值必须为false@AssertFalse boolean isUnsupported;
@AssertTrue字段或属性的值必须为true@AssertTrue boolean isActive;
@DecimalMax字段或属性的值必须是小于或等于值元素中数字的十进制值。@DecimalMax("30.00") BigDecimal discount;
@DecimalMin字段或属性的值必须是大于或等于值元素中数字的十进制值。@DecimalMin("5.00") BigDecimal discount;
@Digits字段或属性的值必须是指定范围内的数字。 integer<instance-digits> 元素指定数字的最大整数位数, fraction<fraction-digits> 元素指定数字的最大小数位数。@Digits(integer=6, fraction=2) BigDecimal price;
@Email该字段或属性的值必须是有效的电子邮件地址。@Email String emailaddress;
@Future该字段或属性的值必须是未来的日期。@Future Date eventDate;
@FutureOrPresent该字段或属性的值必须是现在或未来的日期或时间。@FutureOrPresent Time travelTime;
@Max字段或属性的值必须是小于或等于值元素中数字的整数值。@Max(10) int quantity;
@Min字段或属性的值必须是大于或等于值元素中数字的整数值。@Min(5) int quantity;
@Negative该字段或属性的值必须为负数。@Negative int basementFloor;
@NegativeOrZero该字段或属性的值必须为负数或零。@NegativeOrZero int debtValue;
@NotBlank字段或属性的值必须至少包含一个非空白字符。@NotBlank String message;
@NotEmpty字段或属性的值不能为空。系统会评估字符集或数组的长度,以及集合或映射的大小。@NotEmpty String message;
@NotNull字段或属性的值不能为空。@NotNull String username;
@Null该字段或属性的值必须为空。@Null String unusedString;
@Past该字段或属性的值必须是过去的日期。@Past Date birthday;
@PastOrPresent该字段或属性的值必须是过去或现在的日期或时间。@PastOrPresent Date travelDate;
@Pattern字段或属性的值必须与regexp元素中定义的正则表达式匹配。@Pattern(regexp="\(\d{3}\)\d{3}-\d{4}") String phoneNumber;
@Positive该字段或属性的值必须为正数。@Positive BigDecimal area;
@PositiveOrZero该字段或属性的值必须为正数或零。@PositiveOrZero int totalGoals;
@Size字段或属性的大小将被评估,并且必须与指定的边界相匹配。如果字段或属性是字符串String,则评估字符串的大小。如果字段或属性是数组,则评估数组的大小Collection。使用可选的 <bounds> <bounds> 元素来指定边界。Collection`MapMapmax`min@Size(min=2, max=240) String briefMessage;

在以下示例中,使用内置约束对字段施加@NotNull约束:

public class Name {
    @NotNull
    private String firstname;

    @NotNull
    private String lastname;
}

您还可以对单个 JavaBeans 组件对象设置多个约束。例如,您可以对 <field>`firstname lastname` 字段的大小设置额外的约束:

public class Name {
    @NotNull
    @Size(min=1, max=16)
    private String firstname;

    @NotNull
    @Size(min=1, max=16)
    private String lastname;
}

高级特性

重复注释

从 Bean Validation 2.0 开始,您可以使用重复注解在验证目标上多次指定相同的约束:

public class Account {

    @Max (value = 2000, groups = Default.class, message = "max.value")
    @Max (value = 5000, groups = GoldCustomer.class, message = "max.value")
    private long withdrawalAmount;
}

这样可以通过不同的接口方法,使用不同的分组注解,实现不同场景的差异化校验。

完整实例

import javax.validation.constraints.NotNull;
import java.math.BigDecimal;

public class Order {
    public interface CreateGroup {}    // 下单场景
    public interface UpdateGroup {}    // 修改订单
    public interface CancelGroup {}    // 取消订单
    public interface PayGroup {}       // 支付场景
    public interface ApproveGroup {}   // 审批场景
    
    @NotNull(groups = {CreateGroup.class, UpdateGroup.class}, message = "订单号不能为空")
    private String orderNo;
    
    @NotNull(groups = {CreateGroup.class, PayGroup.class}, message = "金额不能为空")
    private BigDecimal amount;
    
    @NotNull(groups = CancelGroup.class, message = "取消原因不能为空")
    private String cancelReason;
    
    @NotNull(groups = ApproveGroup.class, message = "审批人不能为空")
    private String approver;
}

通过在注解中明确指定使用的分组 @Validated(XxxGroup.class) 来对对象使用不同的校验策略

import com.zzyl.nursing.dto.Order;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/order")
public class OrderController {

    /**
     * 下单 - 使用 CreateGroup
     */
    @PostMapping("/create")
    public String create(@Validated(Order.CreateGroup.class) @RequestBody Order order) {
        return "下单成功,订单号:" + order.getOrderNo();
    }

    /**
     * 修改订单 - 使用 UpdateGroup
     */
    @PutMapping("/update")
    public String update(@Validated(Order.UpdateGroup.class) @RequestBody Order order) {
        return "修改成功,订单号:" + order.getOrderNo();
    }

    /**
     * 取消订单 - 使用 CancelGroup
     */
    @PostMapping("/cancel")
    public String cancel(@Validated(Order.CancelGroup.class) @RequestBody Order order) {
        return "取消成功,原因:" + order.getCancelReason();
    }

    /**
     * 支付 - 使用 PayGroup
     */
    @PostMapping("/pay")
    public String pay(@Validated(Order.PayGroup.class) @RequestBody Order order) {
        return "支付成功,金额:" + order.getAmount();
    }

    /**
     * 审批 - 使用 ApproveGroup
     */
    @PostMapping("/approve")
    public String approve(@Validated(Order.ApproveGroup.class) @RequestBody Order order) {
        return "审批成功,审批人:" + order.getApprover();
    }
}

注意:当你要校验 普通方法参数(不是 @RequestBody 对象)时,可以这样校验

@RestController
@RequestMapping("/order")
@Validated
public class OrderController {

    @GetMapping("/detail")
    public String detail(
            @NotNull Long id,
            @Min(1) Integer page) {
        return "ok";
    }
}

自定义约束

Jakarta Bean Validation 定义了注解、接口和类,允许开发人员创建自定义约束。

使用内置约束创建新约束

Jakarta Bean Validation 包含多个内置约束,这些约束可以组合起来创建新的、可重用的约束。这简化了约束定义,允许开发人员定义一个由多个内置约束组成的自定义约束,然后使用单个注解将其应用于组件属性。

完整例子

这个是内置注解约束 @Pattern

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package javax.validation.constraints;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)//可以定义多个regexp
@Documented
@Constraint(
    validatedBy = {}
)
public @interface Pattern {
    String regexp();//定义正则的字段

    Flag[] flags() default {};

    String message() default "{javax.validation.constraints.Pattern.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    public static enum Flag {
        UNIX_LINES(1),
        CASE_INSENSITIVE(2),
        COMMENTS(4),
        MULTILINE(8),
        DOTALL(32),
        UNICODE_CASE(64),
        CANON_EQ(128);

        private final int value;

        private Flag(int value) {
            this.value = value;
        }

        public int getValue() {
            return this.value;
        }
    }

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Pattern[] value();
    }
}

自定义注解约束

@Pattern.List({
  /* A number of format “+1-NNN-NNN-NNNN” */
  @Pattern(regexp = "\\+1-\\d{3}-\\d{3}-\\d{4}")
})//这里加上内置字段定义电话号的校验格式
@Constraint(validatedBy = {})//用于将一个自定义注解标记为约束注解
 //validatedBy = {} 指定由哪个校验器类(实现了 ConstraintValidator 接口的类)来执行实际的校验逻辑
 //当 {} 为空时:表示没有关联的校验器实现类,校验逻辑必须完全由其他手段提供
 //例如:注解本身只是一个标记(如 @USPhoneNumber 只是一个复合注解,内部用了 @Pattern,实际校验由 @Pattern 完成)
@Documented
@Target({ElementType.METHOD,
    ElementType.FIELD,
    ElementType.ANNOTATION_TYPE,
    ElementType.CONSTRUCTOR,
    ElementType.PARAMETER,
    ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
public @interface USPhoneNumber {

  String message() default "Not a valid US Phone Number";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  @Target({ElementType.METHOD,
     ElementType.FIELD,
     ElementType.ANNOTATION_TYPE,
     ElementType.CONSTRUCTOR,
     ElementType.PARAMETER,
     ElementType.TYPE_USE })
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  @interface List {
  USPhoneNumber[] value();

  }
}

自定义注解之后 使用时不需要每次使用@Pattern(regexp = "\\+1-\\d{3}-\\d{3}-\\d{4}")一长串的注解对字段进行约束

可以使用更加见名知意的注解进行约束

@USPhoneNumber
protected String phone;

自定义校验类

不依赖其他注解的校验类 自己定义参数 自己定义校验方法

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = USPhoneNumberValidator.class)  // 指定校验器类
@Documented
public @interface USPhoneNumber {
    
    String message() default "Not a valid US Phone Number";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
    
    // 自定义属性
    boolean required() default true; //是否需要校验
    String format() default "+1-NNN-NNN-NNNN";//校验的字符串
}

校验类

  1. 类在 classpath 下(任何包都可以)
  2. 有无参构造器(默认就有)
  3. 实现了 ConstraintValidator 接口
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

public class USPhoneNumberValidator implements ConstraintValidator<USPhoneNumber, String> {
    
    private boolean required;
    private String format;
    
    @Override
    public void initialize(USPhoneNumber constraintAnnotation) {
        // 获取注解中的参数值
        this.required = constraintAnnotation.required();
        this.format = constraintAnnotation.format();
        // 可以在这里做一些初始化工作
    }
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 处理 null 或空值
        if (value == null || value.trim().isEmpty()) {
            return !required;  // 如果非必填,返回 true;如果必填,返回 false
        }
        
        // 正则表达式校验美国电话号码
        String regex = "^\\+1-\\d{3}-\\d{3}-\\d{4}$";
        boolean isValid = Pattern.matches(regex, value);
        
        if (!isValid) {
            // 可以自定义错误消息中的动态参数
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(
                String.format("Phone number must follow format: %s", format)
            ).addConstraintViolation();
        }
        
        return isValid;
    }
}

使用自定义注解

public class User {
    
    @USPhoneNumber(required = true, message = "请填写有效的美国电话号码")
    private String phoneNumber;
    
    @USPhoneNumber(required = false)  // 可选字段
    private String alternatePhone;
    
    // getter/setter
}

测试类触发校验

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

public class Test {
    public static void main(String[] args) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        //Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
              //validator.validate(dept);
        User user = new User();
        user.setPhoneNumber("123-456-7890");  // 错误格式
        
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        
        for (ConstraintViolation<User> violation : violations) {
            System.out.println(violation.getMessage());
            // 输出: 请填写有效的美国电话号码
        }
    }
}

注解触发校验

  // 支持分组校验
    public void createUser(@Validated(Default.class) User user) {
        // ...
    }

补充:如果在 Spring 环境中,可以让校验器支持 Spring 的依赖注入:

@Component  // 让 Spring 管理
public class USPhoneNumberValidator implements ConstraintValidator<USPhoneNumber, String> {
    
    @Autowired
    private PhoneNumberService phoneService;  // 可以注入其他服务
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 可以调用 Service 进行更复杂的校验(如查询数据库验证号码是否存在)
        return phoneService.isValidUSNumber(value);
    }
}

还需要配置 Spring 使用自己的 ConstraintValidatorFactory

@Configuration
public class ValidationConfig {
    
    @Bean
    public Validator validator(SpringConstraintValidatorFactory springFactory) {
        try (ValidatorFactory factory = Validation.byDefaultProvider()
                .configure()
                .constraintValidatorFactory(springFactory)  // 关键配置
                .buildValidatorFactory()) {
            return factory.getValidator();
        }
    }
    
    @Bean
    public SpringConstraintValidatorFactory springConstraintValidatorFactory(
            AutowireCapableBeanFactory beanFactory) {
        return new SpringConstraintValidatorFactory(beanFactory);
    }
}

级联验证

在校验 Department 时,也顺带校验 employees 列表中的每一个 Employee对象。

Employee对象里面的校验规则

public class Employee {
    @USPhoneNumber
    private String phone;  // 手机号必须符合格式
    
    @NotBlank
    private String name;
}
public class Department {
    @NotBlank
    private String deptName;
     
    @Valid  // 级联验证 或者使用@Validated
    private List<Employee> employees;
}

校验

@PostMapping("/dept")
public void create(@Validated @RequestBody Department dept) { }

补充 Validated与Valid的区别

@Valid 是标准,@Validated 是 Spring 的扩展

主要是一个支持分组 一个不支持 如果你的代码中 @Validated 没指定分组,等价于默认分组,效果和 @Valid 完全一样

特性@Valid@Validated
来源Jakarta EE 标准Spring 框架
级联验证支持支持
分组验证不支持支持(核心优势)
放在方法参数上可以可以(但类需要加@Validated
放在类上不能可以(开启方法级别验证)
放在字段上及联校验可以(级联)可以(级联)

注意

  1. @Validated 是 Spring 注解 基于AOP切面实现的,级联校验只在 Spring 管理的校验上下文中生效(比如 Controller、Service 参数上的 @Valid/@Validated 触发的校验链路)。如果直接调用 javax.validation.Validator.validate(department),只有 @Valid 会被识别,@Validated 不会被处理。
  2. 级联校验是一个链式反应,@Validated / @Valid 加在字段上只是定义了链的传导规则,本身不会主动触发任何校验。必须有一个外部动作启动校验,链条才会传导下去。简单来说就是如果Department对象不执行校验 他内部的Employee对象也不会自己执行及联校验 因为没有启动入口
  3. @Valid 本身不做任何校验,它只是一个信号,告诉运行时的校验框架"请对这个位置做级联校验"。有没有人读取这个信号,决定了它是否生效。@Valid 并不是加上就一定会自动校验,它必须在 Spring 或 Validation 框架触发校验的场景下 才会生效。

    @RestController
    @Validated
    public class UserController {
      @PostMapping
      public void save(@RequestBody @Valid UserDTO dto) {
      }
    }

    Spring MVC 会进行参数绑定,然后执行 Bean Validation

@RequestBody 参数

@PostMapping("/user")
public void create(@RequestBody @Valid UserDTO dto) {
}

普通对象参数(Form表单)

@PostMapping("/user")
public void create(@Valid UserDTO dto) {
}

这个直接调用不会进行校验 因为没有参数绑定

userService.save(dto);
@Service
public class UserService {

    public void save(@Valid UserDTO dto) {
    }
}

@Valid/@Validated需要依赖依赖 Spring 的 Method Validation AOP触发校验

@Service
@Validated
public class UserService {

    public void save(@Valid/@Validated UserDTO dto) {
    }
}

另外返回值也可以校验 只是没有什么用

@Service
@Validated
public class UserService {

    @Valid
    public UserDTO getUser() {
        return dto;
    }
}

Spring Validation 支持对 List 类型的参数进行容器元素校验,需要满足:

  1. UserDTO 类内部的字段上需要有校验注解(如 @NotNull@Size 等)
  2. 需要触发容器元素校验(Spring Boot 2.3+/Spring 5+ 默认支持)
// UserDTO 定义
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    private String name;
    
    @Min(18)
    private Integer age;
}

// Controller
@PostMapping
public void save(@RequestBody @Valid List<UserDTO> users) {
    // 如果 users 中有任何一个元素校验失败,会抛出 MethodArgumentNotValidException
}

如果spring版本过低需要及联校验

@PostMapping
public void save(@RequestBody @Valid List<@Valid UserDTO> users) {
    // @Valid List<@Valid UserDTO> 中的第二个 @Valid 在某些版本中是显式声明
    // 但现代 Spring Boot 中 @Valid List<UserDTO> 已足够
}