zoukankan      html  css  js  c++  java
  • 自己定义一个注解

    前言

    在代码中会使用到校验 @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

  • 相关阅读:
    如何多个router 进行合并?
    钉钉微应用开发
    vscode 常用命令行
    window.location.search 为何在url 带# 号时获取不到 ?
    如何在嵌套的app中运用vue去写单页面H5
    两秒内不能重复点击
    linux系统下安装dubbo-admin
    二、SpringBoot实现上传文件到fastDFS文件服务器
    一、手把手教你docker搭建fastDFS文件上传下载服务器
    idea中git远程版本回退
  • 原文地址:https://www.cnblogs.com/wei57960/p/12726289.html
Copyright © 2011-2022 走看看