Spring MVC 自定义校验注解
当标准校验注解无法满足业务需求时,可通过自定义校验注解实现特定校验逻辑。
自定义注解三要素
- 注解定义:定义注解及必要属性
- 校验器实现:实现ConstraintValidator接口
- 注册使用:在Bean上使用自定义注解
注解定义模板
Java
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = XxxValidator.class) // 指定校验器
public @interface Xxx {
// 必须包含的三个属性
String message() default "默认错误消息"; // 错误消息
Class<?>[] groups() default {}; // 校验分组
Class<? extends Payload>[] payload() default {}; // 负载信息
// 可选的自定义属性
String value() default "";
}
手机号校验注解
Java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
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 PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 空值由@NotBlank处理
}
return PHONE_PATTERN.matcher(value).matches();
}
}
身份证号校验注解
Java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValidator.class)
public @interface IdCard {
String message() default "身份证号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class IdCardValidator implements ConstraintValidator<IdCard, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true;
}
// 简单校验:18位数字或17位数字+X
if (!value.matches("^\\d{17}[\\dXx]$")) {
return false;
}
// 详细校验逻辑...
return validateIdCard(value);
}
private boolean validateIdCard(String idCard) {
// 校验区域码、出生日期、校验码等
// 省略详细实现
return true;
}
}
带参数的校验注解
Java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = RangeValidator.class)
public @interface Range {
String message() default "值必须在{min}和{max}之间";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
long min() default 0; // 最小值参数
long max() default Long.MAX_VALUE; // 最大值参数
}
public class RangeValidator implements ConstraintValidator<Range, Number> {
private long min;
private long max;
@Override
public void initialize(Range constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
@Override
public boolean isValid(Number value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
long num = value.longValue();
return num >= min && num <= max;
}
}
使用示例
Java
@Data
public class UserDTO {
@Phone(message = "手机号格式不正确")
private String phone;
@IdCard(message = "身份证号格式不正确")
private String idCard;
@Range(min = 0, max = 100, message = "年龄必须在0-100之间")
private Integer age;
}
多类型校验器
Java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotEmptyValidator.class)
public @interface CustomNotEmpty {
String message() default "不能为空";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class NotEmptyValidator implements ConstraintValidator<CustomNotEmpty, Object> {
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
if (value instanceof String) {
return !((String) value).isEmpty();
}
if (value instanceof Collection) {
return !((Collection<?>) value).isEmpty();
}
if (value instanceof Map) {
return !((Map<?, ?>) value).isEmpty();
}
if (value.getClass().isArray()) {
return Array.getLength(value) > 0;
}
return true;
}
}
动态错误消息
Java
public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true;
}
if (!value.matches("^1\\d{10}$")) {
// 禁用默认消息,使用自定义消息
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"手机号 '" + value + "' 格式不正确,应为11位数字"
).addConstraintViolation();
return false;
}
return true;
}
}
跨字段校验注解
Java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
public @interface PasswordMatch {
String message() default "两次密码不一致";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String passwordField() default "password";
String confirmField() default "confirmPassword";
}
public class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, Object> {
private String passwordField;
private String confirmField;
@Override
public void initialize(PasswordMatch constraintAnnotation) {
this.passwordField = constraintAnnotation.passwordField();
this.confirmField = constraintAnnotation.confirmField();
}
@Override
public boolean isValid(Object object, ConstraintValidatorContext context) {
try {
String password = BeanUtils.getProperty(object, passwordField);
String confirm = BeanUtils.getProperty(object, confirmField);
if (password == null || confirm == null) {
return true;
}
if (!password.equals(confirm)) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(confirmField)
.addConstraintViolation();
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
}
@PasswordMatch(passwordField = "password", confirmField = "confirmPassword")
@Data
public class RegisterDTO {
private String password;
private String confirmPassword;
}
依赖注入校验器
Java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "邮箱已存在";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired
private UserService userService; // 可注入Spring Bean
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null || email.isEmpty()) {
return true;
}
return !userService.existsByEmail(email);
}
}
需配置SpringValidatorFactoryBean才能在校验器中注入Bean。
配置Spring校验工厂
Java
@Configuration
public class ValidatorConfig {
@Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setProviderClass(HibernateValidator.class);
return factoryBean;
}
}
校验注解组合
Java
@NotNull
@Size(min = 6, max = 20)
@Pattern(regexp = "^[A-Za-z0-9]+$")
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Password {
String message() default "密码必须6-20位字母数字";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
要点总结
- 自定义注解必须包含message、groups、payload三个属性
- 使用@Constraint指定校验器类
- ConstraintValidator接口包含initialize和isValid方法
- isValid返回true表示校验通过,返回false表示校验失败
- 可通过context动态设置错误消息
- 类级别注解可实现跨字段校验
- 配置LocalValidatorFactoryBean支持依赖注入
📝 发现内容有误?点击此处直接编辑