Bean Validation 与 Hibernate Validator 数据校验精要

掌握 JSR-380 Bean Validation 规范与 Hibernate Validator 扩展,实现分层校验、自定义约束与国际化错误消息

数据校验是企业级应用的第一道防线,在数据进入业务逻辑之前拦截非法输入,可有效减少系统漏洞和异常。Bean Validation 2.0(JSR-380)与 Hibernate Validator 6.x 提供了强大且灵活的校验机制。

一、核心注解速查

1.1 内置约束注解

注解适用范围说明
@NotNull任意值不能为 null
@NotEmptyString/Collection/Map/数组不能为空串且长度 > 0
@NotBlankString不能为 null 且 trim 后长度 > 0
@Size(min, max)String/Collection/Map/数组长度/大小范围
@Min / @Max数字数值范围
@DecimalMin / @DecimalMaxBigDecimal/String小数范围
@Positive / @PositiveOrZero数字正数/非负数
@Negative / @NegativeOrZero数字负数/非正数
@Digits(int, frac)数字整数位和小数位限制
@Past / @PastOrPresent日期过去时间
@Future / @FutureOrPresent日期未来时间
@Pattern(regexp)String正则匹配
@EmailString邮箱格式
@AssertTrue / @AssertFalseBoolean必须为 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 的健壮性和用户体验。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析