自己定义一个注解

前言

在代码中会使用到校验 @NotEmpty @NotNull @Size 的注解等以及在类上注解@Builder 就可以使其拥有建造者模式的功能,本文主要介绍注解基本概念以及自定义一个注解。

1. 注解的基础知识

1.1 注解的分类

从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。

  1. 编译器使用的注解,这类注解不会编译在.class文件中,它们在编译之后就被编译器扔掉了

    • @Override 让编译器检查该方法是否正确的实现了覆写
    • @SuppressWarnings 告诉编译器忽略此处代码产生的警告
  2. 工具处理.class文件使用的注解,源码级别,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。

  3. 程序在运行时能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。

    • 一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。

1.2 注解的定义

注解案例:

@Target(ElementType.FIELD)
// @Target 定义多个范围
// @Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckSize {

    int min() default 0;

    int max() default Integer.MAX_VALUE;

    String value() default "";

}

Java语言中使用 @interface 来定义注解

注解的参数类似无参数方法,可以用default设定一个默认值(强烈推荐),当你不设置一个默认值时在你注解时就必须给没有default默认值的数赋值。最常用的参数应当命名为value。@GetMapping(value="/api") 和 @GetMapping("/api") 效果一样,方便一些,可以省略value=。

元注解: 有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。

@Target : @Target注解用于定义一个注解的能够被应用于源码中的范围

  • 类或接口:ElementType.TYPE;
  • 字段:ElementType.FIELD;
  • 方法:ElementType.METHOD;
  • 构造方法:ElementType.CONSTRUCTOR;
  • 方法参数:ElementType.PARAMETER。
  • 注解:ElementType.ANNOTATION_TYPE
  • 局部变量:ElementType.LOCAL_VARIABLE
  • 包:ElementType.PACKAGE

@Retention 定义了注解的生命周期

  • 仅编译期:RetentionPolicy.SOURCE;
  • 仅class文件:RetentionPolicy.CLASS;
  • 运行期:RetentionPolicy.RUNTIME。

如果@Retention不存在,则该Annotation默认为CLASS。因为通常我们自定义的Annotation都是RUNTIME,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)这个元注解

@Repeatable 使用@Repeatable这个元注解可以定义Annotation是否可重复。多次注解。

@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

@Target(ElementType.TYPE)
public @interface Reports {
    Report[] value();
}

定义后的使用:

@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}

@Inherited 使用@Inherited定义子类是否可继承父类定义的Annotation。@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效。

也就是一个类被@Inherited注解之后,他的子类也默认也使用了此注解。对于接口之间的继承是无效的。

1.3 注解的处理

在我们注解了之后,在哪添加这个注解的逻辑呢。

前面说过注解的三种运行方式:编译,底层内部,运行时。主要就是运行时,所有主要讨论如何读取RUNTIME类型的注解。

因为注解定义后也是一种class,所有的注解都继承自java.lang.annotation.Annotation,因此,读取注解,需要使用反射API。

Java提供的使用反射API读取Annotation的方法包括:

  • 判断某个注解是否存在于Class、Field、Method或Constructor:
    • Class.isAnnotationPresent(Class)
    • Field.isAnnotationPresent(Class)
    • Method.isAnnotationPresent(Class)
    • Constructor.isAnnotationPresent(Class)
// 判断 @NewCheck 是否存在在 username 字段上
username.isAnnotationPresent(NewCheck.class);
  • 使用反射API读取Annotation:
    • Class.getAnnotation(Class)
    • Field.getAnnotation(Class)
    • Method.getAnnotation(Class)
    • Constructor.getAnnotation(Class)
// 获取 Person 定义的 @Report 注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();

当一个注解(Annotation)注解在类上时,我们可以判断这个类是否被Annotation注解,然后再从中取值。亦或者可以直接取值,然后判断是否为空,然后做逻辑处理。

Class clazz = User.class;
if (clazz.isAnnotationPresent(CheckSize.class)) {
     CheckSize checkSize = clazz.getAnnotation(CheckSize.class);
     ...
}
CheckSize checkSize= clazz.getAnnotation(CheckSize.class);
if(checkSize!=null){
    ...
}

当一个注解(Annotation)注解在方法、字段和构造方法上时,比在类上的稍麻烦一些。因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。如下代码所示

public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}

要读取方法参数的注解,我们先用反射获取Method实例,然后读取方法参数的所有注解:

// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
    if (anno instanceof Range) { // @Range注解
        Range r = (Range) anno;
    }
    if (anno instanceof NotNull) { // @NotNull注解
        NotNull n = (NotNull) anno;
    }
}

2. 自己定义一个注解

定义注解:

@Target(ElementType.FIELD) // 只能注解在字段上
@Retention(RetentionPolicy.RUNTIME) // 生命周期:运行时
public @interface CheckSize { // @interface 注解

    int min() default 0; // 最小长度,默认0

    int max() default Integer.MAX_VALUE; // 最大长度

    String value() default ""; // value() 适合比较常用的值,此处我们只是定义了但没有校验逻辑 

}

校验逻辑:

public class CheckSizeValid {

    public static void check(User user) throws IllegalAccessException {
        // objectClass.getDeclaredFields() 通过反射的方式获取对象声明的所有字段
        for (Field field : user.getClass().getDeclaredFields()) {
            // 通过 field.setAccessible(true) 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常)
            field.setAccessible(true);
            // 获取 filed 定义的 @CheckSize 对象
            CheckSize checkSize = field.getAnnotation(CheckSize.class);
            // 如果存在
            if (checkSize != null) {
                Object obj = field.get(user);
                if (obj instanceof String) {
                    String str = (String) obj;
                    if (str.length() < checkSize.min()) {
                        throw new IllegalArgumentException("illegal field " + field.getName() + " length too short");
                    } else if (str.length() > checkSize.max()) {
                        throw new IllegalArgumentException("illegal field " + field.getName() + " length too long");
                    }
                }
            }
        }

    }

}

现在注解有了,逻辑有了。现在我们可以通过直接通过一下方式来实现让逻辑生效。

public class UserTest {
    public static void main(String[] args) throws IllegalAccessException {
        User user = new User();
        user.setUsername("user");
        user.setPassword("jintianshigehaorizi");
        CheckSizeValid.check(user);
        System.out.println("user:" + user.toString());
    }
}

结果

Exception in thread "main" java.lang.IllegalArgumentException: illegal field username length too short
	at org.tustcs.wei.weibackend.basis.jdk.annotation.CheckSizeValid.check(CheckSizeValid.java:36)
	at org.tustcs.wei.weibackend.basis.jdk.annotation.UserTest.main(UserTest.java:21)

如何校验的不那么直接呢?想想通用逻辑统一做如何处理,通过切面编程AOP吧。此时我们再定义一个注解,这个注解修饰方法,然后通过切面获得参数对象,通过反射获得类,然后之后的逻辑处理就和上面一样了。下面贴一点简略代码演示。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Valid {
}
@Aspect
@Component
@Slf4j
public class AopCheck {

    @Pointcut("@annotation(org.tustcs.wei.weibackend.basis.jdk.annotation.Valid)")
    public void check() {
    }

    @Around("check()")
    public void before(JoinPoint joinPoint) throws IllegalAccessException {
        // 获取参数对象
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            Class<?> objectClass = args[i].getClass();
            // ...
        }
    }

}
// 需要检验的地方添加注解
    @Valid
    public void printUser(User user) {
        System.out.println(user.toString());
    }

从使用的角度说已有现成的校验的轮子,可以通过实现 ConstraintValidator 接口来用。

3. 总结

定义一个注解关键的要素:注解的生命周期(大多数我们选择运行时有效),装饰什么(类,方法还是成员变量上?)。使用@interface 定义类。定义类似函数的成员变量,最后有defalut默认值,把最常用的设为value(),会在使用比较方便。

逻辑处理的时候通过反射获得参数,方法等判断是否有注解修饰,有修饰的话就取出进行逻辑判断处理。

References

posted @ 2020-04-18 15:51  胖大星-  阅读(266)  评论(0编辑  收藏  举报