数据校验是企业级应用的第一道防线,在数据进入业务逻辑之前拦截非法输入,可有效减少系统漏洞和异常。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;
}
七、性能优化与工程实践
7.1 Validator 复用与缓存
Validator 实例是线程安全的,应作为单例复用:
@Configuration
public class ValidationConfig {
@Bean
public Validator validator() {
return Validation.buildDefaultValidatorFactory().getValidator();
}
}
@Service
public class OrderService {
@Autowired
private Validator validator; // 复用单例
public void batchValidate(List<OrderRequest> requests) {
// 批量校验时,避免对每个对象重复构建约束元数据
for (OrderRequest req : requests) {
Set<ConstraintViolation<OrderRequest>> violations = validator.validate(req);
// ...
}
}
}
7.2 快速失败模式
批量场景下,首个验证失败即返回,而非收集全部错误:
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true) // 快速失败
.buildValidatorFactory();
Validator fastValidator = factory.getValidator();
适用场景:
- ✅ 快速失败:高并发接口、批量导入、实时交互
- ❌ 完整失败:表单提交、配置导入(用户需要看到所有错误一次性修正)
7.3 Jakarta Bean Validation 演进
| 版本 | 规范 | Java 包 | Spring Boot 版本 | 关键特性 |
|---|---|---|---|---|
| Bean Validation 1.0 | JSR-303 | javax.validation | < 2.0 | 基础注解、XML 配置 |
| Bean Validation 2.0 | JSR-380 | javax.validation | 2.x | @NotEmpty/@NotBlank、Optional 支持、自定义容器 |
| Jakarta Validation 3.0 | — | jakarta.validation | 3.x | 包迁移至 jakarta,无新特性 |
| Jakarta Validation 3.1 | — | jakarta.validation | 3.2+ | ` specified in Jakarta EE 10 |
迁移注意:Spring Boot 3.x 全面迁移至 jakarta 命名空间,升级时需同步替换所有 javax.validation 和 javax.persistence 依赖。
7.4 防御性编程与验证的边界
校验注解是输入验证层的防线,不能替代业务逻辑中的防御性编程:
@Service
public class TransferService {
public void transfer(@Valid TransferRequest req) {
// 校验层已验证格式(金额>0、账户非空)
// 但防御性编程仍需:
Account from = accountRepo.findById(req.getFromAccount())
.orElseThrow(() -> new BusinessException("账户不存在"));
Account to = accountRepo.findById(req.getToAccount())
.orElseThrow(() -> new BusinessException("目标账户不存在"));
if (from.getBalance().compareTo(req.getAmount()) < 0) {
throw new InsufficientBalanceException();
}
if (from.getStatus() != AccountStatus.ACTIVE) {
throw new AccountFrozenException();
}
// 真正的业务逻辑...
}
}
分层防御模型:
- 格式校验(Bean Validation):非空、长度、正则、范围
- 业务存在性校验(Service 层):ID 是否存在、状态是否允许
- 业务规则校验(Domain 层):余额是否足够、权限是否匹配
- 数据库约束(DAO 层):唯一索引、外键、Check 约束
八、测试策略
8.1 单元测试
@Test
public void shouldRejectInvalidEmail() {
UserDto dto = new UserDto();
dto.setEmail("invalid-email");
Set<ConstraintViolation<UserDto>> violations = validator.validateProperty(dto, "email");
assertThat(violations).hasSize(1);
assertThat(violations.iterator().next().getMessage())
.contains("邮箱格式不正确");
}
@Test
public void shouldPassValidUser() {
UserDto dto = createValidUser();
Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
assertThat(violations).isEmpty();
}
8.2 集成测试(Spring Boot Test)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerValidationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void shouldReturn400WhenEmailInvalid() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\": \"bad\", \"age\": 10}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[?(@.field=='email')].message")
.value("邮箱格式不正确"));
}
}
8.3 参数化测试(JUnit 5)
@ParameterizedTest
@CsvSource({
"'', 用户名不能为空",
"ab, 用户名长度必须在 3-20 之间",
"a@b, 用户名只能包含字母、数字和下划线",
"valid_user123, ''"
})
public void shouldValidateUsername(String username, String expectedMessage) {
UserDto dto = new UserDto();
dto.setUsername(username);
dto.setEmail("test@example.com");
dto.setPassword("Password123");
Set<ConstraintViolation<UserDto>> violations = validator.validateProperty(dto, "username");
if (expectedMessage.isEmpty()) {
assertThat(violations).isEmpty();
} else {
assertThat(violations).extracting(ConstraintViolation::getMessage)
.contains(expectedMessage);
}
}
七、总结
| 能力 | 实现方式 | 适用场景 |
|---|---|---|
| 基础校验 | 内置注解 | 通用格式规则 |
| 分组校验 | @Validated(Group.class) | 增删改查不同规则 |
| 级联校验 | @Valid | 嵌套对象校验 |
| 自定义约束 | @Constraint + Validator | 业务专属规则 |
| 关联校验 | 类级别注解 | 字段间逻辑关系 |
| 国际化 | MessageSource | 多语言应用 |
| 编程式校验 | validator.validate() | 复杂动态校验 |
数据校验是防御式编程的核心实践。在正确的层级应用恰当的校验策略,配合清晰的错误反馈,可显著提升 API 的健壮性和用户体验。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。