Skip to content

数据校验

为什么需要数据校验?

在Web应用中,用户输入的数据可能不符合预期,数据校验可以:

  • 确保数据的完整性和有效性
  • 提高应用的安全性
  • 提供友好的错误提示

Bean Validation (JSR-380)

Spring MVC集成了Bean Validation规范,提供了一套标准的校验注解。

添加依赖

Maven:

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

常用校验注解

1. @NotNull, @NotEmpty, @NotBlank

java
public class User {
    @NotNull(message = "ID不能为空")
    private Long id;
    
    @NotEmpty(message = "用户名不能为空") // 不为null且长度>0
    private String username;
    
    @NotBlank(message = "密码不能为空") // 不为null且trim后长度>0
    private String password;
}

2. @Size - 字符串/集合长度

java
public class User {
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
    private String username;
    
    @Size(min = 1, message = "至少需要一个角色")
    private List<String> roles;
}

3. @Email - 邮箱格式

java
public class User {
    @Email(message = "邮箱格式不正确")
    private String email;
}

4. @Pattern - 正则表达式

java
public class User {
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;
}

5. @Min, @Max - 数值范围

java
public class Product {
    @Min(value = 0, message = "价格不能为负数")
    private BigDecimal price;
    
    @Max(value = 100, message = "折扣不能超过100")
    private Integer discount;
}

6. @DecimalMin, @DecimalMax

java
public class Product {
    @DecimalMin(value = "0.01", message = "价格最低为0.01")
    @DecimalMax(value = "999999.99", message = "价格最高为999999.99")
    private BigDecimal price;
}

7. @Positive, @Negative

java
public class Order {
    @Positive(message = "数量必须为正数")
    private Integer quantity;
    
    @PositiveOrZero(message = "折扣金额必须为非负数")
    private BigDecimal discount;
}

8. @Past, @Future - 日期校验

java
public class User {
    @Past(message = "生日必须是过去的日期")
    private LocalDate birthday;
    
    @Future(message = "截止日期必须是未来的日期")
    private LocalDate deadline;
}

9. @Valid - 嵌套校验

java
public class Order {
    @Valid
    @NotNull(message = "订单项不能为空")
    private List<OrderItem> items;
    
    @Valid
    @NotNull(message = "收货地址不能为空")
    private Address shippingAddress;
}

public class OrderItem {
    @NotNull(message = "商品不能为空")
    private Product product;
    
    @Positive(message = "数量必须为正数")
    private Integer quantity;
}

在Controller中使用

基本使用

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        User savedUser = userService.save(user);
        return ResponseEntity.ok(savedUser);
    }
}

处理校验错误

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<?> createUser(
        @Valid @RequestBody User user,
        BindingResult bindingResult) {
        
        if (bindingResult.hasErrors()) {
            Map<String, String> errors = new HashMap<>();
            bindingResult.getFieldErrors().forEach(error -> 
                errors.put(error.getField(), error.getDefaultMessage())
            );
            return ResponseEntity.badRequest().body(errors);
        }
        
        User savedUser = userService.save(user);
        return ResponseEntity.ok(savedUser);
    }
}

全局异常处理

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
        MethodArgumentNotValidException ex) {
        
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        
        return ResponseEntity.badRequest().body(errors);
    }
    
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Map<String, String>> handleConstraintViolation(
        ConstraintViolationException ex) {
        
        Map<String, String> errors = new HashMap<>();
        ex.getConstraintViolations().forEach(violation -> {
            String propertyPath = violation.getPropertyPath().toString();
            String message = violation.getMessage();
            errors.put(propertyPath, message);
        });
        
        return ResponseEntity.badRequest().body(errors);
    }
}

自定义校验注解

1. 创建注解

java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2. 实现校验器

java
public class PhoneValidator implements ConstraintValidator<Phone, 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.isEmpty()) {
            return true; // 使用@NotBlank单独校验
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

3. 使用自定义注解

java
public class User {
    @Phone(message = "手机号格式不正确")
    private String phone;
}

分组校验

用于不同场景下应用不同的校验规则。

1. 定义分组接口

java
public interface CreateGroup {}
public interface UpdateGroup {}

2. 使用分组

java
public class User {
    @NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
    private Long id;
    
    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "用户名不能为空")
    @Size(min = 3, max = 20, groups = {CreateGroup.class, UpdateGroup.class})
    private String username;
    
    @NotBlank(groups = CreateGroup.class, message = "密码不能为空")
    private String password;
}

3. Controller中指定分组

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<User> createUser(
        @Validated(CreateGroup.class) @RequestBody User user) {
        return ResponseEntity.ok(userService.save(user));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
        @PathVariable Long id,
        @Validated(UpdateGroup.class) @RequestBody User user) {
        return ResponseEntity.ok(userService.update(id, user));
    }
}

方法级校验

1. 启用方法级校验

java
@Configuration
@EnableWebMvc
public class WebConfig {
    
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

2. 使用方法级校验

java
@Service
@Validated
public class UserService {
    
    public User getUserById(@NotNull(message = "用户ID不能为空") Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("用户不存在"));
    }
    
    public void deleteUser(@Min(value = 1, message = "ID必须大于0") Long id) {
        userRepository.deleteById(id);
    }
}

完整示例

实体类

java
@Data
public class UserRegistrationDTO {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 30, message = "密码长度必须在6-30之间")
    private String password;
    
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Phone(message = "手机号格式不正确")
    private String phone;
    
    @Past(message = "生日必须是过去的日期")
    private LocalDate birthday;
    
    @Min(value = 18, message = "年龄必须大于等于18岁")
    @Max(value = 120, message = "年龄必须小于等于120岁")
    private Integer age;
    
    @AssertTrue(message = "必须同意用户协议")
    private Boolean agreeTerms;
    
    // 自定义校验逻辑
    @AssertTrue(message = "两次密码输入不一致")
    public boolean isPasswordMatch() {
        return password != null && password.equals(confirmPassword);
    }
}

Controller

java
@RestController
@RequestMapping("/api/users")
@Slf4j
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @PostMapping("/register")
    public ResponseEntity<?> register(
        @Valid @RequestBody UserRegistrationDTO dto) {
        
        try {
            User user = userService.register(dto);
            return ResponseEntity.ok(user);
        } catch (Exception e) {
            log.error("注册失败", e);
            return ResponseEntity.badRequest().body("注册失败: " + e.getMessage());
        }
    }
}

全局异常处理

java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationExceptions(
        MethodArgumentNotValidException ex) {
        
        log.error("参数校验失败", ex);
        
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> {
            errors.put(error.getField(), error.getDefaultMessage());
        });
        
        ErrorResponse response = ErrorResponse.builder()
            .status(HttpStatus.BAD_REQUEST.value())
            .message("参数校验失败")
            .errors(errors)
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.badRequest().body(response);
    }
}

@Data
@Builder
public class ErrorResponse {
    private int status;
    private String message;
    private Map<String, String> errors;
    private LocalDateTime timestamp;
}

最佳实践

  1. 选择合适的校验注解

    • @NotNull:检查不为null
    • @NotEmpty:检查不为null且不为空(字符串、集合)
    • @NotBlank:检查不为null且trim后不为空(仅字符串)
  2. 提供清晰的错误信息

    java
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
  3. 使用分组校验

    • 针对不同场景使用不同的校验规则
  4. 嵌套对象校验

    • 使用@Valid注解进行级联校验
  5. 自定义校验器

    • 复杂的业务逻辑使用自定义校验注解
  6. 全局异常处理

    • 统一处理校验异常,提供一致的错误响应
  7. 前后端双重校验

    • 前端校验提升用户体验
    • 后端校验保证数据安全

总结

数据校验是Web应用开发中的重要环节,Spring MVC提供了强大的校验框架:

  • 使用标准注解进行常规校验
  • 自定义注解处理特殊需求
  • 分组校验应对不同场景
  • 全局异常处理提供统一的错误响应

合理使用数据校验可以大大提高应用的健壮性和安全性。