全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-18 8 分钟 ✍️ juanwangdev

Spring MVC 自定义校验注解

当标准校验注解无法满足业务需求时,可通过自定义校验注解实现特定校验逻辑。

自定义注解三要素

  1. 注解定义:定义注解及必要属性
  2. 校验器实现:实现ConstraintValidator接口
  3. 注册使用:在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支持依赖注入

📝 发现内容有误?点击此处直接编辑

← 上一篇 Spring MVC 数据格式化
下一篇 → CORS协议详解
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库