Spring Validation校验
使用 JSR 303 (Bean Validation) 校验接口参数
JSR 303
,也称为Bean Validation规范,提供了一种在Java应用程序中执行验证的标准化方式。它允许你通过注解直接在领域或者DTO(数据传输对象)类上定义校验规则。
1. 添加依赖
首先需要在项目中添加相关依赖:
<!-- Spring Boot 项目只需添加这个 starter -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency><!-- 非 Spring Boot 项目需要添加这些 -->
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version>
</dependency>
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.0.13.Final</version>
</dependency>
2. 在实体类上添加校验注解
import javax.validation.constraints.*;public class UserDTO {@NotNull(message = "用户ID不能为空")private Long id;@NotBlank(message = "用户名不能为空")@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")private String username;@Min(value = 18, message = "年龄必须大于等于18岁")@Max(value = 120, message = "年龄必须小于等于120岁")private Integer age;@Email(message = "邮箱格式不正确")private String email;@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$", message = "密码必须包含大小写字母和数字,且长度至少8位")private String password;// getters and setters
}
3. 在 Controller 中使用校验
3.1 校验请求体
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/api/users")
public class UserController {@PostMappingpublic ResponseEntity<String> createUser(@RequestBody @Validated UserDTO userDTO) {// 如果校验失败,会抛出 MethodArgumentNotValidException// 业务逻辑处理return ResponseEntity.ok("用户创建成功");}
}
3.2 校验路径变量和请求参数
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable @Min(1) Long id,@RequestParam @NotBlank String type) {// 业务逻辑return ResponseEntity.ok(userDTO);
}
4. 全局异常处理
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {Map<String, String> errors = new HashMap<>();ex.getBindingResult().getAllErrors().forEach(error -> {String fieldName = ((FieldError) error).getField();String errorMessage = error.getDefaultMessage();errors.put(fieldName, errorMessage);});return ResponseEntity.badRequest().body(errors);}
}
5. 常用校验注解
注解 | 说明 |
---|---|
@NotNull | 值不能为null |
@NotEmpty | 字符串/集合不能为null或空 |
@NotBlank | 字符串不能为null且必须包含至少一个非空白字符 |
@Size | 字符串/集合/数组的大小必须在指定范围内 |
@Min | 数字最小值 |
@Max | 数字最大值 |
@DecimalMin | 小数值最小值 |
@DecimalMax | 小数值最大值 |
@Digits | 数字的整数和小数部分的位数限制 |
@Past | 日期必须在过去 |
@PastOrPresent | 日期必须在过去或现在 |
@Future | 日期必须在未来 |
@FutureOrPresent | 日期必须在未来或现在 |
@Pattern | 字符串必须匹配正则表达式 |
字符串必须是有效的电子邮件地址 | |
@Positive | 数字必须是正数 |
@PositiveOrZero | 数字必须是正数或零 |
@Negative | 数字必须是负数 |
@NegativeOrZero | 数字必须是负数或零 |
6. 分组校验
可以定义不同的校验组,在不同场景下应用不同的校验规则:
public interface CreateGroup {}
public interface UpdateGroup {}public class UserDTO {@Null(groups = CreateGroup.class, message = "创建时ID必须为空")@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")private Long id;// 其他字段...
}@PostMapping
public ResponseEntity<String> createUser(@RequestBody @Validated(CreateGroup.class) UserDTO userDTO) {// 业务逻辑
}
7. 自定义校验注解
当内置注解不能满足需求时,可以自定义校验注解:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;// 自定义注解
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {String message() default "无效的手机号码";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}// 自定义校验规则
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {@Overridepublic boolean isValid(String phoneNumber, ConstraintValidatorContext context) {// 实现校验逻辑return phoneNumber != null && phoneNumber.matches("^1[3-9]\\d{9}$");}
}
使用自定义注解:
public class UserDTO {@ValidPhoneNumberprivate String phone;
}
8. 结合 Hutool 工具自定义
- 身份证号码正确性校验:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IDCard.IDCardCheck.class)
public @interface IDCard {boolean required() default true;String message() default "请输入正确的身份证号码";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};/*** 校验规则*/@Componentclass IDCardCheck implements ConstraintValidator<IDCard, String> {private boolean required;@Overridepublic void initialize(IDCard constraintAnnotation) {this.required = constraintAnnotation.required();}@Overridepublic boolean isValid(String idCard, ConstraintValidatorContext constraintValidatorContext) {// 非必填if (!required) {return true;}// 使用 Hutool 的工具return IdcardUtil.isValidCard(idCard);}}
}
- 电话号码正确性校验:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = Phone.PhoneCheck.class)
public @interface Phone {boolean required() default true;String message() default "请输入正确的手机号码";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};/*** 校验规则*/@Componentclass PhoneCheck implements ConstraintValidator<Phone, String> {private boolean required;@Overridepublic void initialize(Phone constraintAnnotation) {this.required = constraintAnnotation.required();}@Overridepublic boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {// 非必填if (!required) {return true;}// 使用 Hutool 的工具return PhoneUtil.isPhone(phone);}}
}
9. 国际化支持
9.1 创建消息文件
在 src/main/resources
目录下创建文件:
ValidationMessages.properties # 默认消息文件
ValidationMessages_zh_CN.properties # 中文消息文件
ValidationMessages_en_US.properties # 英文消息文件
ValidationMessages_ja_JP.properties # 日文消息文件
9.2 文件内容示例
ValidationMessages.properties:
# 通用消息
user.id.null=用户ID不能为空
user.name.size=用户名长度必须在{min}-{max}个字符之间
user.age.range=年龄必须在{min}到{max}岁之间
user.email.invalid=请输入有效的电子邮件地址
user.password.pattern=密码必须包含大小写字母和数字,且长度至少8位# 自定义注解消息
phone.invalid=手机号格式不正确,请输入11位有效手机号
ValidationMessages_zh_CN.properties:
user.id.null=用户ID不能为空
user.name.size=用户名长度必须在{min}到{max}个字符之间
9.3 在注解中引用消息
public class UserDTO {@NotNull(message = "{user.id.null}")private Long id;@Size(min = 2, max = 20, message = "{user.name.size}")private String username;@Min(value = 18, message = "{user.age.range}")@Max(value = 120, message = "{user.age.range}")private Integer age;@Email(message = "{user.email.invalid}")private String email;@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$", message = "{user.password.pattern}")private String password;@Phone(message = "{phone.invalid}") // 自定义注解private String phone;
}
9.4 参数化消息
消息中可以包含参数,参数会在运行时被替换:
user.name.size=用户名长度必须在{min}-{max}个字符之间
user.age.range=年龄必须在{min}到{max}岁之间
注解中的参数会自动填充到消息中:
@Size(min = 2, max = 20, message = "{user.name.size}")
private String username; // 显示:用户名长度必须在2-20个字符之间
9.5 国际化关键实现
Spring Boot 会自动根据请求的 Accept-Language
头选择对应的消息文件:
- 请求头
Accept-Language: zh-CN
→ 使用ValidationMessages_zh_CN.properties
- 无匹配或默认 → 使用
ValidationMessages.properties
9.5.1 Locale 解析器配置
@Configuration
public class LocaleConfig {// 基于请求头的解析器@Beanpublic LocaleResolver localeResolver() {AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();resolver.setDefaultLocale(Locale.ENGLISH);return resolver;}// 消息源配置@Beanpublic MessageSource messageSource() {ResourceBundleMessageSource source = new ResourceBundleMessageSource();source.setBasename("ValidationMessages");source.setDefaultEncoding("UTF-8");source.setUseCodeAsDefaultMessage(true);return source;}
}
9.5.2 自定义消息插值器
public class I18nMessageInterpolator implements MessageInterpolator {private final MessageSource messageSource;private final LocaleResolver localeResolver;public I18nMessageInterpolator(MessageSource messageSource, LocaleResolver localeResolver) {this.messageSource = messageSource;this.localeResolver = localeResolver;}@Overridepublic String interpolate(String messageTemplate, Context context) {return interpolate(messageTemplate, context, Locale.getDefault());}@Overridepublic String interpolate(String messageTemplate, Context context, Locale locale) {try {// 解析消息键(去掉花括号)if (messageTemplate.startsWith("{") && messageTemplate.endsWith("}")) {String messageKey = messageTemplate.substring(1, messageTemplate.length() - 1);return messageSource.getMessage(messageKey, resolveArguments(context), locale);}return messageTemplate;} catch (NoSuchMessageException e) {return messageTemplate;}}private Object[] resolveArguments(Context context) {// 从校验注解中提取参数(如@Size的min/max)if (context.getConstraintDescriptor().getAnnotation() instanceof Size) {Size size = (Size) context.getConstraintDescriptor().getAnnotation();return new Object[] {context.getPropertyPath().toString(), // 字段名size.max(),size.min()};}return new Object[0];}
}
9.5.3 注册自定义校验器
@Bean
public Validator validator(MessageSource messageSource, LocaleResolver localeResolver) {LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();factoryBean.setMessageInterpolator(new I18nMessageInterpolator(messageSource, localeResolver));return factoryBean;
}
9.5.4 使用建议
- 统一管理:将所有校验消息集中到 ValidationMessages 文件中
- 命名规范:使用
对象.字段.校验类型
的命名方式(如user.email.invalid
) - 避免硬编码:不要在注解中直接写消息内容,全部通过消息键引用
- 参数化消息:利用
{min}
,{max}
等占位符使消息更灵活 - 多语言支持:为每种语言提供单独的消息文件
完毕。