写作时间:2025-08-19

本章介绍实现自定义注解校验 springboot 项目接收前端传入参数。

💡 什么是注解:
Java 注解(Annotation)是一种特殊的标记性代码,它不直接影响程序的执行逻辑,但可以为代码提供元数据(描述数据的数据),供编译器、工具或运行时环境使用。

主要作用

  1. 编译期检查与约束
  2. 简化代码与替代配置文件
  3. 运行时动态处理,如生成动态代码
  4. 作为特定标记,用于告诉编译器一些信息

元注解

java内置的注解,标明该注解的使用范围、生命周期、生效范围等。

  1. @Retention 指定被修饰的注解的生命周期(即注解保留到哪个阶段)
  • 取值:
    • SOURCE: 注解仅在源代码阶段存在,编译时会被丢弃(不会进入 class 文件)。
    • CLASS: 注解在编译时保留到 class 文件中,但 JVM 运行时不会加载(默认值)。
    • RUNTIME: 注解在运行时仍然存在,可通过反射获取。
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可见
public @interface MyAnnotation {}

  1. @Targe指定被修饰的注解可以应用在哪些程序元素上(如类、方法、字段等)。
  • 取值:
    • TYPE:类、接口、枚举、注解。
    • METHOD:方法。
    • FIELD:字段(成员变量、枚举常量)。
    • PARAMETER:方法参数。
    • CONSTRUCTOR:构造方法。
    • LOCAL_VARIABLE:局部变量。
    • ANNOTATION_TYPE:注解类型(只能修饰其他注解)。
    • PACKAGE:包。
    • TYPE_PARAMETER:类型参数(如泛型中的 <T>)。
    • TYPE_USE:任何类型的使用场景(如声明变量、强制类型转换等)。
@Target({ElementType.METHOD, ElementType.FIELD}) // 注解可用于方法和字段
public @interface MyAnnotation {}

  1. @Documented:指定被修饰的注解会被 Javadoc 工具提取到文档中(默认情况下,注解不会出现在文档中)。
@Documented // 该注解会出现在 Javadoc 文档中
public @interface MyAnnotation {}

  1. @Inherited:指定被修饰的注解具有继承性。即如果父类使用了该注解,子类若未显式标注其他注解,则会继承父类的该注解(仅对类注解有效,对方法、字段等无效)。
@Inherited // 允许子类继承该注解
public @interface MyAnnotation {}

@MyAnnotation
class Parent {}

class Child extends Parent {} // Child 会继承 Parent 的 @MyAnnotation 注解

  1. Repeatablle: 指定被修饰的注解是可重复的,即同一个程序元素上可以多次使用该注解(Java 8 新增)。
// 定义容器注解(存储重复的 @MyAnnotation)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {
    MyAnnotation[] value(); // 数组类型,存储重复的注解
}

// 标记 @MyAnnotation 为可重复(关联容器注解)
@Repeatable(MyAnnotations.class)
public @interface MyAnnotation {
    String value();
}

// 重复使用注解
@MyAnnotation("value1")
@MyAnnotation("value2")
class MyClass {}

  1. @Native:标记注解的成员变量是一个与 native 方法相关的常量(Java 8 新增)。通常用于生成 JNI 头文件时识别常量。
public @interface MyAnnotation {
    @Native int value() default 0; // 标记为与 native 方法相关的常量
}

在springboot中自定义注解

  1. 创建注解
// 参数校验注解: 用户传入参数中 手机号码 和 邮箱 必须有一个不为空


// 注解作用于类(因为要校验多个字段)
@Target({ElementType.TYPE})
// 注解保留到运行时
@Retention(RetentionPolicy.RUNTIME)
// 标记为约束注解,指定校验逻辑的实现类
@Constraint(validatedBy = OneOfNotNullValidator.class)
public @interface OneOfNotNull {

    // 错误提示信息
    String message() default "phone和email必须至少填写一个";

    // 分组校验(默认空)
    Class<?>[] groups() default {};

    // 负载信息(默认空)
    Class<? extends Payload>[] payload() default {};

    // 必须填写的字段名称(支持多个)
    String[] fields();
}
    

  1. 注解逻辑实现
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Field;

// 泛型:第一个参数是自定义注解,第二个参数是要校验的类(RegisterDTO)
public class OneOfNotNullValidator implements ConstraintValidator<OneOfNotNull, Object> {

    // 存储需要校验的字段名称(如 {"phone", "email"})
    private String[] fields;

    @Override
    public void initialize(OneOfNotNull constraintAnnotation) {
        // 初始化时获取注解中指定的字段名称
        this.fields = constraintAnnotation.fields();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // value 是被校验的对象(如 RegisterDTO 实例)
        if (value == null) {
            return false; // 对象为null时直接校验失败
        }

        try {
            // 遍历所有需要校验的字段
            for (String fieldName : fields) {
                // 通过反射获取字段
                Field field = value.getClass().getDeclaredField(fieldName);
                field.setAccessible(true); // 允许访问私有字段
                // 获取字段值
                Object fieldValue = field.get(value);

                // 如果有一个字段不为空,校验通过
                if (fieldValue != null && !fieldValue.toString().trim().isEmpty()) {
                    return true;
                }
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            // 反射异常(如字段不存在),视为校验失败
            return false;
        }

        // 所有字段都为空,校验失败
        return false;
    }
}
    

  1. 注解使用
@Data
@OneOfNotNull(fields = {"phone", "email"}, message = "手机号和邮箱必须存在一个不为空")
public class RegisterDTO {

    /**
     * 手机号码
     */
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码格式不正确")
    private String phone;

    /**
     * 用户邮箱
     */
    @Email(message = "邮箱格式不正确")
    private String email;
}

  1. 未知代码解释
  • 注解@Constraint的作用:标记自定义校验注解的元注解,用于告诉 Bean Validation 框架:这个注解是一个校验注解,并且指定了该注解的校验逻辑由哪个类实现。
  • Payload 接口作用:这是一个标记接口(空接口),用于在校验注解中传递额外的元数据(如校验的严重程度、业务标识等),本身没有任何方法。
// 1. 对校验约束进行分类或标记(例如区分 “警告级”“错误级” 校验)。
// 2. 校验失败时,可通过 ConstraintViolation.getConstraintDescriptor().getPayload() 
// 获取 payload 信息,用于后续处理(如日志分级、错误页面跳转等)。

// 使用示例:
// 定义一个表示“严重错误”的 payload 类
public class Severity implements Payload {
    public static class Error implements Severity {} // 错误级
    public static class Warning implements Severity {} // 警告级
}

// 在自定义注解中使用
public @interface Phone {
    // 允许指定 payload(默认空)
    Class<? extends Payload>[] payload() default {};
}

// 使用注解时指定 payload
public class User {
    @Phone(payload = Severity.Error.class) // 标记为错误级校验
    private String phone;
}
  • initialize方法:initialize方法是ConstraintValidator接口中的方法,用于初始化校验器,在校验器实例创建后、执行校验逻辑前调用。
// 1. 获取自定义注解中的属性值(如正则表达式、错误消息等),并保存到校验器中,供后续校验使用。
// 2. 执行一些初始化操作(如编译正则表达式、加载配置等)。

// 使用示例:
public class PhoneValidator implements ConstraintValidator<Phone, String> {
    private String regexp; // 存储注解中的正则表达式

    @Override
    public void initialize(Phone constraintAnnotation) {
        // 从注解中获取 regexp 属性值并初始化
        this.regexp = constraintAnnotation.regexp();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 使用 initialize 中获取的 regexp 进行校验
        return value.matches(regexp);
    }
}

// 这里 initialize() 从 @Phone 注解中获取正则表达式,供 isValid() 方法使用。
  • isvalid方法:isvalid方法是ConstraintValidator接口中的核心方法,用于 执行实际的校验逻辑,返回 true 表示校验通过,false 表示校验失败。
// 1. value:被校验的字段值(如手机号字符串、实体对象等)。
// 2. context:校验上下文,可用于修改错误消息、禁用默认消息等。

// 使用示例:
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
    // 校验逻辑:如果手机号为空,或不匹配正则,则返回 false
    if (phone == null || phone.isEmpty()) {
        return false;
    }
    return phone.matches(regexp); // 使用 initialize 中初始化的 regexp
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐