Skip to content

Spring Boot 中使用 Validation

在 Spring Boot 中使用 Validation 可以轻松实现数据校验功能,确保应用程序接收的数据符合预期格式和业务规则。

完整实现方案

1. 添加依赖

pom.xml 中添加必要的依赖:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

2. 创建数据模型并添加校验注解

import javax.validation.constraints.*;

public class UserRegistrationRequest {

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

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

    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", 
             message = "密码必须包含大小写字母和数字,且长度至少8位")
    private String password;

    @Min(value = 18, message = "年龄必须大于或等于18岁")
    @Max(value = 100, message = "年龄必须小于或等于100岁")
    private Integer age;

    @NotNull(message = "出生日期不能为空")
    @Past(message = "出生日期必须是过去的时间")
    private LocalDate birthDate;

    @AssertTrue(message = "必须接受条款和条件")
    private Boolean termsAccepted;

    // Getters and Setters
}

3. 创建控制器并添加校验支持

在Controller的方法参数上使用@Valid@Validated注解来触发校验: 对于路径变量和请求参数的校验,需要使用@Validated注解在类级别

import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

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

    @PostMapping("/register")
    public ResponseEntity<String> registerUser(@Valid @RequestBody UserRegistrationRequest request) {
        // 业务逻辑处理
        return ResponseEntity.ok("用户注册成功");
    }
    // 对于路径变量和请求参数的校验,需要使用@Validated注解在类级别
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable @Min(1) Long id) {
        // 获取用户逻辑
        return ResponseEntity.ok(new User(id, "示例用户"));
    }
}

4. 处理校验异常

当校验失败时,Spring会抛出 - MethodArgumentNotValidException(对于请求体) - ConstraintViolationException(对于路径变量/请求参数)。

可以通过@RestControllerAdvice@ControllerAdvice + @ResponseBody)创建全局异常处理器捕获:

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
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("com.example.demo.controller")
public class GlobalExceptionHandler {


    /**
     * 处理请求体校验失败
     *
     * @param ex 异常
     * @return Result
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> 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);

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

    }

    /**
     * 处理路径变量或请求参数校验失败
     *
     * @param ex 异常
     * @return Result
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<?> handleConstraintViolationExceptions(ConstraintViolationException ex) {

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

        });

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

    }


    /**
     * 处理未知异常
     *
     * @param request 请求参数
     * @param e       异常
     * @return Result
     */
    @ExceptionHandler(value = Throwable.class)
    public ResponseEntity<?> unknownExceptionHandler(HttpServletRequest request, Throwable e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }

}

5. 常用校验注解说明

注解 功能 示例
@NotNull 值不能为 null @NotNull(message = "字段不能为空")
@NotBlank 字符串不能为空且必须包含非空白字符 @NotBlank(message = "用户名不能为空")
@NotEmpty 集合/数组不能为空 @NotEmpty(message = "列表不能为空")
@Size 检查字符串/集合大小 @Size(min=2, max=10, message="长度2-10")
@Min 数字最小值 @Min(value=18, message="必须≥18")
@Max 数字最大值 @Max(value=100, message="必须≤100")
@Email 邮箱格式验证 @Email(message="邮箱格式无效")
@Pattern 正则表达式匹配 @Pattern(regexp="[A-Za-z]+", message="仅字母")
@Past 日期必须在过去 @Past(message="必须是过去日期")
@Future 日期必须在未来 @Future(message="必须是未来日期")
@AssertTrue 值必须为 true @AssertTrue(message="必须接受条款")
@AssertFalse 值必须为 false @AssertFalse(message="不能是已注册用户")

6. 高级用法:分组校验

当同一个Bean在不同场景下需要不同的校验规则时,可以使用分组校验。

1. 定义分组接口:

public interface CreateGroup {}

public interface UpdateGroup {}

2. 在Bean上指定分组:

public class UserDTO {

    @Null(groups = CreateGroup.class) // 创建时id必须为null

    @NotNull(groups = UpdateGroup.class) // 更新时id不能为null

    private Long id;

    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class})

    private String name;

}

3. 在Controller方法中使用@Validated指定分组:

@PostMapping
public String createUser(@Validated(CreateGroup.class) @RequestBody UserDTO userDTO) {

// ...

}

@PutMapping
public String updateUser(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) {

// ...

}

分组校验完整示例代码如下:

// 定义校验分组接口
public interface CreateValidationGroup {}
public interface UpdateValidationGroup {}

public class Product {
    @Null(groups = CreateValidationGroup.class, 
           message = "ID必须为空")
    @NotNull(groups = UpdateValidationGroup.class, 
             message = "ID不能为空")
    private Long id;

    @NotBlank(groups = {CreateValidationGroup.class, UpdateValidationGroup.class}, 
              message = "产品名称不能为空")
    private String name;

    // getters and setters
}

// 在控制器中使用分组校验
@PostMapping
public ResponseEntity<String> createProduct(
        @Validated(CreateValidationGroup.class) @RequestBody Product product) {
    // 创建产品逻辑
    return ResponseEntity.ok("产品创建成功");
}

@PutMapping("/{id}")
public ResponseEntity<String> updateProduct(
        @PathVariable Long id,
        @Validated(UpdateValidationGroup.class) @RequestBody Product product) {
    // 更新产品逻辑
    return ResponseEntity.ok("产品更新成功");
}

7. 高级用法:自定义校验器

当内置注解不满足需求时,可以自定义校验注解。

示例:自定义手机号校验

1. 创建自定义注解:

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

@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {
    String message() default "无效的手机号码";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2. 实现校验逻辑

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.trim().isEmpty()) {
            return true; // 允许为空,配合@NotBlank使用
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

3. 在模型中使用

public class ContactInfo {
    @ValidPhoneNumber(message = "请输入有效的手机号码")
    private String phone;
}

8. 高级用法:嵌套对象校验

在Spring Boot Validation中,需要使用@Valid注解来触发嵌套对象的校验。

假设我们有一个类Order,其中包含一个Customer类型的字段,而Customer对象本身也有需要校验的字段。

以下是具体步骤:

  1. 定义嵌套对象(例如Customer类)并添加校验注解。

  2. 在包含嵌套对象的类(例如Order类)中,在嵌套对象字段上添加@Valid注解。

  3. 在控制器方法中,使用@Valid触发校验。

示例代码:

步骤1:定义嵌套对象(Customer)

import javax.validation.constraints.NotBlank;

import javax.validation.constraints.Size;

public class Customer {

    @NotBlank(message = "客户名称不能为空")
    @Size(min = 2, max = 30, message = "客户名称长度必须在2到30之间")
    private String name;

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

    // 省略getter和setter

}

步骤2:定义包含嵌套对象的类(Order)

import javax.validation.Valid;

import javax.validation.constraints.NotNull;

import java.math.BigDecimal;

public class Order {

    @NotNull(message = "订单号不能为空")
    private String orderId;

    @NotNull(message = "订单金额不能为空")
    @DecimalMin(value = "0.01", message = "订单金额必须大于0")
    private BigDecimal amount;

    // 关键:使用@Valid触发嵌套校验
    @Valid
    @NotNull(message = "客户信息不能为空")
    private Customer customer;

    // 省略getter和setter

}

步骤3:在控制器中使用

import org.springframework.validation.annotation.Validated;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController

public class OrderController {

    @PostMapping("/orders")
    public String createOrder(@Valid @RequestBody Order order) {

        // 处理订单
        return "订单创建成功";

    }

}

步骤4:全局异常处理器(处理校验异常)

如果之前已经创建了全局异常处理器,它会自动处理嵌套校验的错误。例如,我们之前定义的GlobalExceptionHandler已经可以处理MethodArgumentNotValidException,它会收集所有层级的错误。

当校验失败时,返回的响应可能如下:

{
    "customer.name": "客户名称不能为空",
    "customer.email": "邮箱格式不正确",
    "amount": "订单金额必须大于0"
}

注意:嵌套对象可以多层嵌套,只需要在每一层需要校验的对象字段上加上@Valid注解即可。

例如,如果Customer中还有一个Address对象需要校验:

public class Customer {

    // ... 其他字段
    @Valid
    private Address address;

}

这样,校验就会递归到Address对象。

9. 复杂嵌套场景处理

场景 1:集合中的嵌套对象

public class OrderRequest {
    // ...

    @Valid
    @NotEmpty(message = "至少需要一个订单项")
    private List<OrderItem> items;
}

public class OrderItem {
    @NotBlank(message = "商品ID不能为空")
    private String productId;

    @Min(value = 1, message = "数量至少为1")
    private Integer quantity;

    @DecimalMin(value = "0.01", message = "价格必须大于0")
    private BigDecimal price;
}

场景 2:多层嵌套对象

public class CompanyOrder extends OrderRequest {
    @Valid
    @NotNull(message = "发票信息不能为空")
    private Invoice invoice;
}

public class Invoice {
    @NotBlank(message = "发票抬头不能为空")
    private String title;

    @Pattern(regexp = "\\d{15}|\\d{18}", message = "税号格式不正确")
    private String taxNumber;

    @Valid
    private BankAccount bankAccount;
}

public class BankAccount {
    @NotBlank(message = "开户行不能为空")
    private String bankName;

    @Pattern(regexp = "\\d{16,19}", message = "账号格式不正确")
    private String accountNumber;
}

场景 3:条件性嵌套校验

使用分组校验实现条件性嵌套校验:

// 定义校验分组
public interface PersonalOrderGroup {}
public interface CompanyOrderGroup {}

public class OrderRequest {
    // ...

    @NotNull(message = "订单类型不能为空")
    private OrderType orderType;

    @Valid
    @NotNull(groups = PersonalOrderGroup.class, 
             message = "个人客户信息不能为空")
    private Customer personalCustomer;

    @Valid
    @NotNull(groups = CompanyOrderGroup.class, 
             message = "企业客户信息不能为空")
    private Company company;
}

// 在控制器中指定分组
@PostMapping("/personal")
public ResponseEntity<String> createPersonalOrder(
        @Validated(PersonalOrderGroup.class) @RequestBody OrderRequest order) {
    // 处理个人订单
}

@PostMapping("/company")
public ResponseEntity<String> createCompanyOrder(
        @Validated(CompanyOrderGroup.class) @RequestBody OrderRequest order) {
    // 处理企业订单
}

嵌套校验的关键是在对象字段上使用@Valid注解(或@Validated,但通常用@Valid),这样校验框架就会递归地校验该对象内部的约束。

Spring Validation使用总结

  1. 避免过度嵌套
  2. 超过3层嵌套建议重构
  3. 复杂校验逻辑移到服务层

  4. 性能优化

    • 避免在 @Pattern 中使用复杂正则表达式
    • 对于大型对象,优先在服务层进行深度校验
    • 配置Validation发现第一个错误即返回
      @Configuration
      public class ValidationConfig {
          @Bean
          public Validator validator() {
              return Validation.byProvider(HibernateValidator.class)
                      .configure()
                      .failFast(true) // 发现第一个错误即返回
                      .buildValidatorFactory()
                      .getValidator();
          }
      }
      
  5. 结合DTO模式

  6. 为不同API创建专用的请求DTO
  7. 避免实体类污染校验注解

嵌套校验是Spring Validation的强大功能,合理使用可以: 1. 保持数据结构的清晰性 2. 实现复杂的校验逻辑 3. 提供精细的错误反馈 4. 减少控制器和服务层的校验代码