数据校验是企业级应用的第一道防线,在数据进入业务逻辑之前拦截非法输入,可有效减少系统漏洞和异常。Bean Validation 2.0(JSR-380)与 Hibernate Validator 6.x 提供了强大且灵活的校验机制。
一、核心注解速查
1.1 内置约束注解
| 注解 | 适用范围 | 说明 |
|---|---|---|
@NotNull | 任意 | 值不能为 null |
@NotEmpty | String/Collection/Map/数组 | 不能为空串且长度 > 0 |
@NotBlank | String | 不能为 null 且 trim 后长度 > 0 |
@Size(min, max) | String/Collection/Map/数组 | 长度/大小范围 |
@Min / @Max | 数字 | 数值范围 |
@DecimalMin / @DecimalMax | BigDecimal/String | 小数范围 |
@Positive / @PositiveOrZero | 数字 | 正数/非负数 |
@Negative / @NegativeOrZero | 数字 | 负数/非正数 |
@Digits(int, frac) | 数字 | 整数位和小数位限制 |
@Past / @PastOrPresent | 日期 | 过去时间 |
@Future / @FutureOrPresent | 日期 | 未来时间 |
@Pattern(regexp) | String | 正则匹配 |
@Email | String | 邮箱格式 |
@AssertTrue / @AssertFalse | Boolean | 必须为 true/false |
@Valid | 对象/集合 | 级联校验 |
1.2 基础用法
public class UserRegistrationRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在 3-20 之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 32, message = "密码长度必须在 8-32 之间")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
message = "密码必须包含大小写字母和数字")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄必须大于等于 18 岁")
@Max(value = 120, message = "年龄必须小于等于 120 岁")
private Integer age;
@NotNull(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@NotNull(message = "生日不能为空")
@Past(message = "生日必须是过去的时间")
private LocalDate birthday;
@Valid // 级联校验
@NotNull(message = "地址不能为空")
private Address address;
}
public class Address {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
@NotBlank(message = "详细地址不能为空")
@Size(max = 200, message = "详细地址不能超过 200 字")
private String detail;
}
二、Spring Boot 集成
2.1 Controller 层校验
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<Void> register(
@Valid @RequestBody UserRegistrationRequest request // @Valid 触发校验
) {
userService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@GetMapping
public List<User> list(
@RequestParam @Min(0) @Max(1000) Integer page,
@RequestParam @Min(1) @Max(100) Integer size
) {
return userService.findPage(page, size);
}
@GetMapping("/{userId}")
public User getUser(
@PathVariable @Pattern(regexp = "^\\d{10}$") String userId
) {
return userService.findById(userId);
}
}
2.2 统一异常处理
@RestControllerAdvice
public class ValidationExceptionHandler {
// 处理 @Valid 校验失败
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> new FieldError(
error.getField(),
error.getDefaultMessage(),
error.getRejectedValue()
))
.collect(Collectors.toList());
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "参数校验失败", errors));
}
// 处理 @RequestParam / @PathVariable 校验失败
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex) {
List<FieldError> errors = ex.getConstraintViolations().stream()
.map(v -> new FieldError(
v.getPropertyPath().toString(),
v.getMessage(),
v.getInvalidValue()
))
.collect(Collectors.toList());
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "参数校验失败", errors));
}
}
2.3 分组校验
public interface ValidationGroups {
interface Create {} // 创建场景
interface Update {} // 更新场景
interface Delete {} // 删除场景
}
public class UserDto {
@Null(groups = ValidationGroups.Create.class, message = "创建时 ID 必须为空")
@NotNull(groups = ValidationGroups.Update.class, message = "更新时 ID 不能为空")
private Long id;
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})
private String username;
@NotBlank(groups = ValidationGroups.Create.class)
private String password;
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})
private String email;
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public void create(@Validated(ValidationGroups.Create.class) @RequestBody UserDto dto) {
userService.create(dto);
}
@PutMapping("/{id}")
public void update(@Validated(ValidationGroups.Update.class) @RequestBody UserDto dto) {
userService.update(dto);
}
}
三、自定义约束注解
3.1 手机号校验
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // @NotNull/@NotBlank 处理空值
}
return PATTERN.matcher(value).matches();
}
}
// 使用
public class UserDto {
@Phone
private String phone;
}
3.2 枚举值校验
@Documented
@Constraint(validatedBy = EnumValueValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumValue {
Class<? extends Enum<?>> enumClass();
String message() default "值不在允许的枚举范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {
private Set<String> validValues;
@Override
public void initialize(EnumValue annotation) {
validValues = Arrays.stream(annotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toSet());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
return validValues.contains(value);
}
}
// 使用
public enum OrderStatus {
PENDING, PAID, SHIPPED, COMPLETED, CANCELLED
}
public class OrderQuery {
@EnumValue(enumClass = OrderStatus.class)
private String status;
}
3.3 字段关联校验
@Documented
@Constraint(validatedBy = DateRangeValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidDateRange {
String message() default "结束时间必须晚于开始时间";
String startField();
String endField();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class DateRangeValidator implements ConstraintValidator<ValidDateRange, Object> {
private String startField;
private String endField;
@Override
public void initialize(ValidDateRange annotation) {
this.startField = annotation.startField();
this.endField = annotation.endField();
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context) {
try {
LocalDateTime start = (LocalDateTime) new PropertyDescriptor(startField, obj.getClass())
.getReadMethod().invoke(obj);
LocalDateTime end = (LocalDateTime) new PropertyDescriptor(endField, obj.getClass())
.getReadMethod().invoke(obj);
if (start == null || end == null) return true;
return end.isAfter(start);
} catch (Exception e) {
return false;
}
}
}
// 使用
@ValidDateRange(startField = "startTime", endField = "endTime")
public class EventCreateRequest {
@NotNull
private LocalDateTime startTime;
@NotNull
private LocalDateTime endTime;
}
四、Service 层校验
4.1 编程式校验
@Service
public class OrderService {
@Autowired
private Validator validator;
public void createOrder(OrderCreateRequest request) {
// 手动触发校验
Set<ConstraintViolation<OrderCreateRequest>> violations = validator.validate(request);
if (!violations.isEmpty()) {
String message = violations.stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.joining("; "));
throw new ValidationException(message);
}
// 业务逻辑...
}
public void updateOrder(Long id, OrderUpdateRequest request) {
// 指定分组校验
Set<ConstraintViolation<OrderUpdateRequest>> violations =
validator.validate(request, ValidationGroups.Update.class);
if (!violations.isEmpty()) {
throw new ValidationException("参数校验失败");
}
// 特定字段快速校验
Set<ConstraintViolation<OrderUpdateRequest>> priceViolation =
validator.validateProperty(request, "price");
}
}
4.2 方法参数校验(AOP)
@Service
@Validated // 开启方法参数校验
public class UserService {
public User createUser(
@NotBlank String username,
@Email String email,
@Min(18) @Max(120) int age
) {
// 参数会在调用前自动校验
return userDao.save(new User(username, email, age));
}
public void updateStatus(
@NotNull Long userId,
@Pattern(regexp = "ACTIVE|INACTIVE|BANNED") String status
) {
userDao.updateStatus(userId, status);
}
}
五、国际化错误消息
5.1 配置消息源
spring:
messages:
basename: validation-messages
encoding: UTF-8
# validation-messages.properties(默认)
user.username.notblank=Username is required
user.email.invalid=Please enter a valid email address
# validation-messages_zh.properties(中文)
user.username.notblank=用户名不能为空
user.email.invalid=请输入有效的邮箱地址
5.2 自定义消息解析
public class I18nMessageInterpolator implements MessageInterpolator {
@Autowired
private MessageSource messageSource;
@Override
public String interpolate(String messageTemplate, Context context) {
return interpolate(messageTemplate, context, LocaleContextHolder.getLocale());
}
@Override
public String interpolate(String messageTemplate, Context context, Locale locale) {
if (messageTemplate.startsWith("{")) {
String key = messageTemplate.substring(1, messageTemplate.length() - 1);
return messageSource.getMessage(key, null, messageTemplate, locale);
}
return messageTemplate;
}
}
5.3 Hibernate Validator 配置
@Configuration
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setValidationMessageSource(messageSource);
return factoryBean;
}
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(validator);
return processor;
}
}
六、最佳实践
6.1 分层校验策略
Controller 层:格式校验(非空、长度、格式)
↓
Service 层:业务校验(存在性、状态、权限)
↓
DAO 层:数据库约束(唯一性、外键)
6.2 校验规则封装
public class ValidationPatterns {
public static final String PHONE = "^1[3-9]\\d{9}$";
public static final String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,32}$";
public static final String USERNAME = "^[a-zA-Z0-9_]{3,20}$";
public static final String ID_CARD = "^(\\d{15}|\\d{18}|\\d{17}[Xx])$";
}
public class ValidationMessages {
public static final String PHONE_INVALID = "手机号格式不正确";
public static final String PASSWORD_WEAK = "密码强度不足";
}
6.3 容器校验
public class BatchCreateRequest {
@NotEmpty(message = "至少需要一个订单")
@Size(max = 100, message = "单次最多创建 100 个订单")
@Valid // 校验集合中每个元素
private List<@Valid OrderCreateRequest> orders;
}
// Java 8+ 支持容器元素注解
public class ScoreMap {
private Map<@NotBlank String, @Min(0) @Max(100) Integer> scores;
}
七、总结
| 能力 | 实现方式 | 适用场景 |
|---|---|---|
| 基础校验 | 内置注解 | 通用格式规则 |
| 分组校验 | @Validated(Group.class) | 增删改查不同规则 |
| 级联校验 | @Valid | 嵌套对象校验 |
| 自定义约束 | @Constraint + Validator | 业务专属规则 |
| 关联校验 | 类级别注解 | 字段间逻辑关系 |
| 国际化 | MessageSource | 多语言应用 |
| 编程式校验 | validator.validate() | 复杂动态校验 |
数据校验是防御式编程的核心实践。在正确的层级应用恰当的校验策略,配合清晰的错误反馈,可显著提升 API 的健壮性和用户体验。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。