B站视频链接:https://www.bilibili.com/video/BV1p4411P7V3/?p=3

1、注解简介

Annotation是jdk5.0开始引入的新技术。注解的格式:@注解名,比如@Override
Annotation的作用:

  • 注解不是程序本身,可以对程序作出解释,或者强调被注解标注的方法或属性;
  • 可以被其他程序读取(比如编译器读取);
  • 注解还可以检查程序是否满足某些约束。

Annotation的格式:

  • 注解是以”@注解名”在代码中存在的,还可以添加一些参数值,例如@SuppressWarnings(value="unchecked")

Annoation在哪里使用:

  • 可以附加在package、class、method、field等上面,相当于给他们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问。

    2、内置注解

  • @Override:定义java.lang.Override中,此注解仅适用于修饰方法,表示子类打算覆写父类里对应的方法;

  • @Deprecated:定义在java.lang.Deprecated,此注释可以用于修饰方法、属性和类,表示不鼓励程序员使用这些元素,通常是因为它将要被废弃或者存在风险或者有更好的选择;
  • @SuppressWarnings:定义在java.lang.SuppressWarnings中,用来抑制编译时的告警信息,最常用的就是@SuppressWarnings(value="unchecked"),丰哥说代码里最好不要出现这个;

    3、元注解

    元注解的作用是负责注解其他注解,Java定义了4个标准的meta-annotation类型,他们被用来提供对其他annotation类型做说明。4个标准的元注解为:

  • @Target:用于描述注解的使用范围,即被描述的注解可以用在什么地方;

  • @Retention:表示需要在什么级别保存该注解信息,用于描述注解的生命周期;
  • @Documented:说明该注解将被包含在javadoc中;
  • @Inherited:说明子类可以继承父类中的该注解。

    3.1 @Target

    以Lombok为例,@Data注解就是作用在类上的,@Setter注解是作用在类、方法、属性上的,@Target注解就是描述注解的使用范围,有以下五个位置:

  • ElementType.TYPE :可以作用在类、接口和枚举类上;

  • ElementType.METHOD :可以作用在方法上;
  • ElementType.FIELD :可以作用在成员变量上;
  • ElementType.CONSTRUCTOR :可以作用在构造器上;
  • ElementType.LOCAL_VARIABLE :可以作用在局部变量上。

    3.2 @Retention

    @Retention表示注解的生命周期,即注解被保留的阶段,有三种枚举值:

  • RetentionPolicy.SOURCE :在源文件中有效(即源文件保留),编译时编译器会直接丢弃这种策略的注解;

  • RetentionPolicy.CLASS : 在class文件中有效(即class保留),当运行Java程序时, JVM不会保留注解。这是默认值;
  • RetentionPolicy.RUNTIME : 在运行时有效(即运行时保留),当运行 Java 程序时, JVM会保留注解。程序可以通过反射获取该注解。

    3.3 @Documented

    用的很少。

    3.4 @Inherited

    @Inherited也是用来修饰其他的Annotation的,被修饰过的Annotation将具有继承性。。。
    例子:
  1. @xxx是我自定义的注解,我现在使用@xxx注解在Base类上使用….
  2. 使用@Inherited修饰@xxx注解
  3. 当有类继承了Base类的时候,该实现类自动拥有@xxx注解

    4、自定义注解

    使用@Interface自定义注解时,自动继承了java.lang.annotation.Annotation接口。
    需要注意以下几点:
  • @Interface用来声明一个注解,格式为:public @interface 注解名{ 定义内容 },当在类内定义注解时不需要public
  • 其中的每一个方法实际上是声明了一个配置参数;
  • 方法的名称就是参数的名称;
  • 返回值的类型就是参数的类型,返回值只能是基本类型;
  • 可以通过default来声明参数的默认值;
  • 如果只有一个参数成员,注解里仅写这一个参数成员即可,不需要value;如果有多个参数成员,注解里是value={value1, value2…},即value后是一个枚举类型的数组,也可以直接{value1, value2…}不需要value关键字;
  • 注解元素必须要有值,我们定义注解元素时,经常使用空字符串、0作为默认值。

举个例子,Demo1里的注解有多种类型的入参。
注解:

  1. @Target({ElementType.METHOD, ElementType.TYPE})
  2. @Retention(value= RetentionPolicy.RUNTIME)
  3. public @interface MyAnno {
  4. /*
  5. * 1、这个不是方法,而是注解里要传入的参数,参数类型 + 参数名;
  6. * 2、default是当注解里没有传入参数时,默认指定的参数,一般跟0或者"",如果默认值为-1代表不存在
  7. */
  8. String name() default "";
  9. int age() default 0;
  10. // 如果默认值为-1,代表不存在
  11. int id() default -1;
  12. // 参数可以是数组,即注解里传入多个value
  13. String[] schools() default {};
  14. }

使用:

@MyAnno(name="Jerry", schools = {"uestc", "swjtu"})
public class User {
    @MyAnno(age = 27)
    public void method(){}
}

当注解的入参只需要一个参数的时候,建议用value命名参数名,可以省略value=XXX(如果参数名不是value则不行),直接将参数传入注解中,如下:
注解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(value= RetentionPolicy.RUNTIME)
public @interface MyAnno2 {
    // 仅有一个参数建议用value命名参数名
    String value() default "";
}

使用:

public class User {
    @MyAnno2("Jerry")
    public void method2(){}
}

5、自定义注解的使用

上面介绍了如何自定义一个注解,那通过自定义注解如何在不侵入代码的前提下让注解生效呢?一般有两种方案:

  • 注解 + 注解处理器
  • 注解 + aop

第二种方案可以参考我这篇文章中的第二节使用demo:https://www.yuque.com/zhangjian-mbxkb/zu7dxg/xe9hy1
本节对第一种方案的使用做个demo。

5.1 需求澄清

有一个用户的实体类,类里有用户名称和用户密码,都是字符串类型,要求对用户名和用户密码长度做限制,这里采用注解 + 注解处理器的方案实现。
用户类:

package com.Jerry.annotitaion;

/**
 * @Description:
 * @Author: Jerry
 * @Date: 2022-03-09 22:24
 */
public class UserInfo {
    @Check(min = 2, max = 3, description = "用户名长度在2-10个字符之间")
    private String name;

    @Check(min = 6, max = 10, description = "用户密码长度在6-10个字符之间")
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

说明:

  • @Check注解就是我们自定义的对用户字段校验的注解。

    5.2 注解 + 注解处理器

    注解Check: ```java package com.Jerry.annotitaion;

import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;

/**

  • @author 11048 */ @Target({ElementType.FIELD}) @Retention(value= RetentionPolicy.RUNTIME) public @interface Check { int max() default 0; int min() default 0; String description() default “”; } ``` 说明:
  • 由于注解处理器是基于反射实现的,因此注解Check要在JVM运行期也要生效,因此Check注解的@Retention的值为RetentionPolicy.RUNTIME;
  • Check注解支持三个参数,max代表字符串最大长度,min代表字符串最小长度,description是限制条件描述。

注解处理器CheckAnnoHandler:

package com.Jerry.annotitaion;

import java.lang.reflect.Field;

/**
 * @Description:
 * @Author: Jerry
 * @Date: 2022-03-09 22:26
 */
public class CheckAnnoHandler {
    public void handle(Object obj) throws IllegalArgumentException, IllegalAccessException {
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field: fields) {
            Check check = field.getAnnotation(Check.class);
            if (check == null) {
                continue;
            } else {
                field.setAccessible(true);
                if ("class java.lang.String".equals(field.getGenericType().toString())) {
                    String value = String.valueOf(field.get(obj));
                    if (value != null) {
                        if (value.length() < check.min() || value.length() > check.max()) {
                            throw new IllegalArgumentException("argument must between min and max");
                        }
                    }
                }
            }
        }
    }
}

说明:

  • 既然有了自定义注解,一定要有对应的注解处理器,自定义注解的作用才能生效;
  • 在这个注解处理器实现中,使用到了反射的api在运行期间获取检查的UserInfo实例,通过获取属性field和对应的注解值来实现,需要对熟悉反射的api。

Main类:

package com.Jerry.annotitaion;

/**
 * @Description:
 * @Author: Jerry
 * @Date: 2022-03-09 22:39
 */
public class Main {
    public static void main(String[] args) {
        CheckAnnoHandler checkAnnoHandler = new CheckAnnoHandler();
        UserInfo userInfo = new UserInfo();
        userInfo.setName("jerry");
        userInfo.setPassword("121");
        try {
            checkAnnoHandler.handle(userInfo);
        } catch (IllegalAccessException e) {
        }
    }
}

运行结果:
image.png
感觉还是没有注解 + aop的实现简洁,使用方不需要再显示地调用注解处理器,注解处理器的逻辑都在切面里实现。

6、注解相关问题

6.1 自定义的Java注解是如何生效的?

自定义注解后,需要定义这个注解的注解解析及处理器,在这个注解解析及处理器的内部,通过反射机制使用Class、Method、Field对象的getAnnotation()方法可以获取各自位置上的注解信息,基于反射api获取类实例中的信息及注解中传入的参数值,进而完成注解的逻辑,例如给属性赋值、查找依赖的对象实例等。

6.2 如果要自定义一个编译期生效的注解,如何实现?

自定义注解的生命周期在编译期的,声明这个注解时@Retention的值为RetentionPolicy.CLASS,需要明确的是此时注解信息保留在源文件和字节码文件中,在JVM加载class文件后,注解信息不会存在内存中。

6.3 有如下的一个自定义注解,在使用这个注解的时候,它的value值是存在哪的?

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
     String value() default "";
}

注解在编译期间编译成了接口,这个接口继承了java.lang.annotation.Annotation接口,接口是不能直接实例化使用的,当在代码中使用这个注解,并使用getAnnotation方法获取注解信息时,JVM通过动态代理的方式生成一个实现了注解接口的代理对象实例,然后对该实例的属性赋值,value值就存在这个代理对象实例中。

参考

Java注解是如何玩转的,面试官和我聊了半个小时
Java注解基本用法