前言
参数校验是 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;
}
总结
参数校验要点:
- ✅ 基础注解 - @NotBlank、@Email、@Min、@Max、@Size
- ✅ Controller 使用 - @Valid、@Validated
- ✅ 自定义校验器 - 实现 ConstraintValidator
- ✅ 跨字段校验 - 类级别注解
- ✅ 分组校验 - 不同场景使用不同分组
- ✅ 编程式校验 - Validator 接口
- ✅ 异常处理 - 全局处理校验异常
良好的参数校验,能有效防止非法数据进入系统。