Skip to content
清晨的一缕阳光
返回

Spring Boot 参数校验实战

前言

参数校验是 Web 开发中的第一道防线。Spring Boot 集成了 Hibernate Validator,提供了强大的参数校验能力。本文将介绍参数校验的完整用法和最佳实践。

基础校验注解

常用注解

@Data
public class UserDTO {
    
    /**
     * 不能为空
     */
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    /**
     * 邮箱格式
     */
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    /**
     * 数值范围
     */
    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄最小为 1")
    @Max(value = 150, message = "年龄最大为 150")
    private Integer age;
    
    /**
     * 字符串长度
     */
    @Size(min = 6, max = 20, message = "密码长度 6-20 位")
    private String password;
    
    /**
     * 正则表达式
     */
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    /**
     * 日期范围
     */
    @Past(message = "生日必须是过去")
    private LocalDate birthday;
    
    /**
     * 未来日期
     */
    @Future(message = "预约时间必须是未来")
    private LocalDateTime appointmentTime;
    
    /**
     * 嵌套校验
     */
    @Valid
    private AddressDTO address;
}

地址 DTO

@Data
public class AddressDTO {
    
    @NotBlank(message = "省份不能为空")
    private String province;
    
    @NotBlank(message = "城市不能为空")
    private String city;
    
    @NotBlank(message = "详细地址不能为空")
    @Size(min = 5, max = 100, message = "详细地址长度 5-100")
    private String detail;
    
    @Pattern(regexp = "^\\d{6}$", message = "邮编格式不正确")
    private String zipCode;
}

Controller 中使用

1. @RequestBody 校验

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    
    /**
     * 创建用户
     */
    @PostMapping
    public R<UserDTO> create(
        @Valid @RequestBody UserDTO dto
    ) {
        return R.ok(userService.create(dto));
    }
    
    /**
     * 批量创建
     */
    @PostMapping("/batch")
    public R<List<UserDTO>> batchCreate(
        @Valid @RequestBody List<UserDTO> dtos
    ) {
        return R.ok(userService.batchCreate(dtos));
    }
}

2. @RequestParam 校验

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    /**
     * 查询参数校验
     */
    @GetMapping
    public R<List<UserDTO>> getUsers(
        @RequestParam @Min(1) int page,
        @RequestParam @Min(1) @Max(100) int size,
        @RequestParam(required = false) @Email String email
    ) {
        return R.ok(userService.findAll(page, size, email));
    }
}

3. @PathVariable 校验

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    /**
     * 路径参数校验
     */
    @GetMapping("/{id}")
    public R<UserDTO> getUser(
        @PathVariable @Min(1) Long id
    ) {
        return R.ok(userService.findById(id));
    }
    
    /**
     * 正则校验
     */
    @GetMapping("/code/{code}")
    public R<UserDTO> getByCode(
        @PathVariable @Pattern(regexp = "^\\d{6}$") String code
    ) {
        return R.ok(userService.findByCode(code));
    }
}

4. @ModelAttribute 校验

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    /**
     * 表单数据校验
     */
    @PostMapping("/form")
    public R<UserDTO> createFromForm(
        @Valid @ModelAttribute UserDTO dto
    ) {
        return R.ok(userService.create(dto));
    }
}

自定义校验器

1. 创建校验注解

@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. 实现校验器

public class PhoneValidator implements ConstraintValidator<Phone, String> {
    
    private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) {
            return true; // null 值由@NotNull/@NotBlank 处理
        }
        return value.matches(PHONE_REGEX);
    }
}

3. 使用自定义注解

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

跨字段校验

1. 创建类级别注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
@Documented
public @interface PasswordMatch {
    
    String message() default "两次密码输入不一致";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
    
    String passwordField() default "password";
    
    String confirmField() default "confirmPassword";
}

2. 实现校验器

public class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, Object> {
    
    private String passwordField;
    private String confirmField;
    
    @Override
    public void initialize(PasswordMatch annotation) {
        this.passwordField = annotation.passwordField();
        this.confirmField = annotation.confirmField();
    }
    
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        try {
            Field passwordField = value.getClass().getDeclaredField(this.passwordField);
            passwordField.setAccessible(true);
            String password = (String) passwordField.get(value);
            
            Field confirmField = value.getClass().getDeclaredField(this.confirmField);
            confirmField.setAccessible(true);
            String confirmPassword = (String) confirmField.get(value);
            
            boolean valid = Objects.equals(password, confirmPassword);
            
            if (!valid) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                    .addPropertyNode(this.confirmField)
                    .addConstraintViolation();
            }
            
            return valid;
        } catch (Exception e) {
            return false;
        }
    }
}

3. 使用

@Data
@PasswordMatch(message = "两次密码输入不一致")
public class UserRegisterDTO {
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度 6-20 位")
    private String password;
    
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
}

分组校验

1. 定义分组

public interface Groups {
    
    interface Create {}
    
    interface Update {}
    
    interface Admin {}
}

2. 使用分组

@Data
public class UserDTO {
    
    // 创建时不需要
    @Null(groups = Groups.Create.class)
    private Long id;
    
    // 创建和更新都需要
    @NotBlank(message = "用户名不能为空", groups = {Groups.Create.class, Groups.Update.class})
    private String username;
    
    // 只有创建时需要
    @NotBlank(message = "密码不能为空", groups = Groups.Create.class)
    @Size(min = 6, max = 20, message = "密码长度 6-20 位", groups = Groups.Create.class)
    private String password;
    
    // 只有管理员能修改
    @Null(groups = {Groups.Create.class, Groups.Update.class})
    private Boolean isAdmin;
}

3. Controller 中使用

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    @PostMapping
    public R<UserDTO> create(
        @Validated(Groups.Create.class) @RequestBody UserDTO dto
    ) {
        return R.ok(userService.create(dto));
    }
    
    @PutMapping("/{id}")
    public R<UserDTO> update(
        @PathVariable Long id,
        @Validated(Groups.Update.class) @RequestBody UserDTO dto
    ) {
        return R.ok(userService.update(id, dto));
    }
}

编程式校验

1. 使用 Validator

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final Validator validator;
    
    public UserDTO create(UserDTO dto) {
        // 编程式校验
        Set<ConstraintViolation<UserDTO>> violations = validator.validate(dto);
        if (!violations.isEmpty()) {
            String message = violations.stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining("; "));
            throw new BusinessException("VALIDATION_ERROR", message);
        }
        
        // 业务逻辑
        return dto;
    }
}

2. 手动校验

@Service
public class UserService {
    
    public UserDTO create(UserDTO dto) {
        // 手动校验
        if (dto.getUsername() == null || dto.getUsername().isEmpty()) {
            throw new BusinessException("用户名不能为空");
        }
        
        if (dto.getEmail() != null && !dto.getEmail().matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new BusinessException("邮箱格式不正确");
        }
        
        return dto;
    }
}

校验异常处理

1. 全局处理

@RestControllerAdvice
public class ValidationExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining("; "));
        
        return R.error(400, message);
    }
    
    @ExceptionHandler(ConstraintViolationException.class)
    public R<Void> handleConstraintViolation(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream()
            .map(ConstraintViolation::getMessage)
            .collect(Collectors.joining("; "));
        
        return R.error(400, message);
    }
}

最佳实践

1. DTO 与 Entity 分离

// ✅ 推荐
@PostMapping
public R<UserDTO> create(@Valid @RequestBody UserCreateDTO dto) {
    return R.ok(userService.create(dto));
}

// ❌ 不推荐
@PostMapping
public R<User> create(@Valid @RequestBody User user) {
    return R.ok(userService.save(user));
}

2. 校验提示国际化

# messages.properties
NotBlank.username=用户名不能为空
Email.email=邮箱格式不正确
Min.age=年龄最小为{value}
Max.age=年龄最大为{value}
Size.password=密码长度{min}-{max}位

3. 使用 Record(JDK 16+)

// JDK 21 Record
public record UserCreateDTO(
    @NotBlank(message = "用户名不能为空") String username,
    @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") String email,
    @NotNull(message = "年龄不能为空") @Min(value = 1, message = "年龄最小为 1") Integer age
) {}

4. 嵌套对象校验

@Data
public class OrderDTO {
    
    @NotNull(message = "用户不能为空")
    @Valid
    private UserDTO user;
    
    @NotNull(message = "商品列表不能为空")
    @NotEmpty(message = "商品列表不能为空")
    @Valid
    private List<ProductDTO> products;
}

总结

参数校验要点:

良好的参数校验,能有效防止非法数据进入系统。


分享这篇文章到:

上一篇文章
MySQL 主从延迟分析与解决
下一篇文章
SkyWalking 性能指标监控