Java 注解

用注解来保存类相关的信息以供反射调用

提供了一种为程序元素设置元数据的方法

所有注解都隐式地继承自 java.lang.annotation.Annotation 接口

注解的功能

  • 作为特定的标记,告诉编译器一些信息

例如 @Override 注解修饰的方法,编译器会检查是否重写

  • 编译时动态处理,如动态生成代码

典型的如 @Data 注解

  • 运行时动态处理,作为额外信息的载体,如获取注解信息

这类注解在运行时依旧有效,生命周期为RUNTIME

注解的分类

  • 标准注解:@Override,@Deprecated,@SuppressWarnings
  • 元注解:@Retention,@Target,@Inherited,@Documented
  • 自定义注解

元注解

用于修饰注解的注解,通常用在注解的定义上

元注解 功能
@Target 定义注解的作用目标
@Retention 定义注解的生命周期
@Documented 定义注解是否应当被包含在JavaDoc文档中
@Inherited 定义是否允许子类继承该注解

@Target

Target 接口中定义了一个 ElementType 数组,用来描述注解可以应用的元素类型

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * 返回注释接口可应用于的元素类型的数组。
     */
    ElementType[] value();
}

ElementType 枚举类中定义可以被注解修饰的元素类型

public enum ElementType {
    /** 类,接口(包括注解接口), 枚举, 或记录
     * declaration */
    TYPE,

    /** 成员变量(包括枚举常量) */
    FIELD,

    /** 方法 */
    METHOD,

    /** 方法参数 */
    PARAMETER,

    /** 构造器 */
    CONSTRUCTOR,

    /** 本地局部变量 */
    LOCAL_VARIABLE,

    /** 注解 */
    ANNOTATION_TYPE,

    /** 包 */
    PACKAGE,

    /**
     * 用于泛型参数
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE,

    /**
     * Module declaration.
     *
     * @since 9
     */
    MODULE,

    /**
     * Record component
     *
     * @jls 8.10.3 Record Members
     * @jls 9.7.4 Where Annotations May Appear
     *
     * @since 16
     */
    RECORD_COMPONENT;
}

@Retention

指示带注释接口的注释要保留多长时间。如果注释接口声明中不存在Retention注释,则保留策略默认为RetentionPolicy.CLASS

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * 返回保留策略
     */
    RetentionPolicy value();
}

RetentionPolicy 定义了所有保留策略

public enum RetentionPolicy {
    /**
     * 注解在源文件保留,在编译后的class文件中不保留,典型的有 @Override, @Data
     */
    SOURCE,

    /**
     * 注解会在源文件和class文件中保留,但不会在VM运行时保留
     */
    CLASS,

    /**
     * 注释将由编译器记录在类文件中,并在运行时由VM保留,因此可以反射性地读取它们。
     * 典型的有 @Autowired,让程序在运行时通过反射的方法将bean实例注入
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

自定义注解

@interface 声明创建了一个真正的Java接口

自定义注解的修饰符支持 public缺省, 后者定义的注解只能在同一个包下使用

注解不能实现和继承其他接口或类

注解定义格式如下,一般自定义注解要定义 @Target,@Retention 来指定注解修饰的元素类型和注解的生命周期

因为注解是由编译器计算而来的,因此,所有元素值必须是编译期常量

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    // 修饰符 返回值 属性名() 默认值
    public String value() default "default"; 
}

下面定义一个包含课程信息的注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CourseInfoAnnotation {
    //课程名称
    public String courseName();
    //课程标签
    public  String courseTag();
    //课程简介
    public String courseProfile();
    //课程序号
    public int courseIndex() default 303;
}

用这个注解修饰一个类和类中的方法

@CourseInfoAnnotation(
        courseName = "英语",
        courseTag = "必修",
        courseProfile = "英语模块"
)
public class MyCourse {
    @CourseInfoAnnotation(
            courseName = "英语听说",
            courseTag = "必修",
            courseProfile = "",
            courseIndex = 1)
    public void getCourseInfo() {

    }
}

注解的解析

注解自身并不会做任何事情,例如我们常用的@Test注解,就需要有JUnit工具支持,JUnit可能会调用所有标识为@Test的方法,这个过程中就用到了反射机制来获取注解信息。

我们看一下反射相关的类(FieldConstructorMethod)和注解相关类之间的关系,如下图所示,所有反射对象的基类都实现了 AnnotatedElement接口。

image.png

  • AnnotatedElement :表示当前在此VM中运行的程序的注释元素,MethodConstructorFieldClassPackage都实现了这个接口,这个接口定义了判断注解是否存在和多种获取注解的方法 image.png
  • AccessibleObjectAccessibleObject类是FieldMethodConstructor对象(称为反射对象)的基类。它提供了将反射对象标记为在使用时禁止检查Java语言访问控制的能力。这允许具有足够权限的复杂应用程序(如Java对象序列化或其他持久性机制)以通常被禁止的方式操作对象。

反射解析注解的思路:

  1. 获取这个类的Class对象
  2. 通过Class对象获取类上的属性、方法
  3. 调用AnnotatedElement中的方法获取注解类
    下面是用反射获取注解信息的例子
public class MyAnnotationParser {
    public static void processAnnotations(Object obj) {
        // 由实例对象获取Class对象
        Class<?> cl = obj.getClass();

        // 获取类上修饰的注解
        CourseInfoAnnotation courseInfoAnnotationOnClass = cl.getAnnotation(CourseInfoAnnotation.class);
        
        if (courseInfoAnnotationOnClass != null) {
            System.out.println("---类注解---");
            System.out.println("courseName = " + courseInfoAnnotationOnClass.courseName());
            System.out.println("courseProfile = " + courseInfoAnnotationOnClass.courseProfile());
            System.out.println("courseTag = " + courseInfoAnnotationOnClass.courseTag());
            System.out.println("courseIndex = " + courseInfoAnnotationOnClass.courseIndex());
        }
        
        // 获取方法上修饰的注解
        for (Method method : cl.getMethods()) {
            CourseInfoAnnotation courseInfoAnnotationOnMethod = method.getAnnotation(CourseInfoAnnotation.class);
            if (courseInfoAnnotationOnMethod != null) {
                System.out.println("---方法注解---");
                System.out.println("courseName = " + courseInfoAnnotationOnMethod.courseName());
                System.out.println("courseProfile = " + courseInfoAnnotationOnMethod.courseProfile());
                System.out.println("courseTag = " + courseInfoAnnotationOnMethod.courseTag());
                System.out.println("courseIndex = " + courseInfoAnnotationOnMethod.courseIndex());
            }
        }

    }
    public static void main(String[] args) {
        MyCourse myCourse = new MyCourse();
        processAnnotations(myCourse);
    }
}

注解获取属性值的底层实现

  • JVM 会为注解生成代理对象
    添加 JVM 的启动参数 -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true,保留jdk动态代理生成的类文件

运行我们之前注解解析的main函数,在项目目录下生成了一下文件

image.png

其中 $Proxy1 个动态生成的类实现了我们自定义的注解接口,下面主要分析这个类。

public final class $Proxy1 extends Proxy implements CourseInfoAnnotation {
    // 列举部分方法
    private static Method m1;
    private static Method m4;
    
    // ... 省略其他方法声明 ...
    
    // 在静态代码块中给方法赋值
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m4 = Class.forName("demo.annotation.CourseInfoAnnotation").getMethod("courseName");
            // ... 省略其他方法赋值 ...
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

    public $Proxy1(InvocationHandler var1) throws  {
        super(var1);
    }
    
    // 重写 Object 中的 equals 方法
    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
    
    // 重写自定义注解中的方法
    public final String courseName() throws  {
        try {
            return (String)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

   // ...省略其他的方法重写...

动态代理

涉及到动态代理,这里简单介绍一下动态代理中常见的类

  • Proxy : 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。
  • InvocationHandler: 由代理实例的调用处理程序实现的接口, 只声明了一个 invoke(Object proxy, Method method, Object[] args)方法来处理代理实例上的方法。

重写方法的执行过程

看一下代理类重写方法后做了什么,以 courseName() 方法为例

public final String courseName() throws  {
   try {
       return (String)super.h.invoke(this, m4, (Object[])null);
   } catch (RuntimeException | Error var2) {
       throw var2;
   } catch (Throwable var3) {
       throw new UndeclaredThrowableException(var3);
   }
}

super 是父类 ProxyhInvocationHandler变量,调用了 invoke方法来处理代理实例上的方法。

这里调用的InvocationHanler 接口的实现类是 AnnotationInvocationHandler ,实现类的 invoke方法实现如下:

// 存储注解里的属性名和属性值
private final Map<String, Object> memberValues;

public Object invoke(Object proxy, Method method, Object[] args) {
    String member = method.getName();
    int parameterCount = method.getParameterCount();

    // Handle Object and Annotation methods
    if (parameterCount == 1 && member == "equals" &&
            method.getParameterTypes()[0] == Object.class) {
        return equalsImpl(proxy, args[0]);
    }
    if (parameterCount != 0) {
        throw new AssertionError("Too many parameters for an annotation method");
    }

    if (member == "toString") {
        return toStringImpl();
    } else if (member == "hashCode") {
        return hashCodeImpl();
    } else if (member == "annotationType") {
        return type;
    }

    // 对于其他方法,从 memberValus 中取值
    Object result = memberValues.get(member);

    // ... 省略异常处理 ...

    return result;
}

注解的工作原理

  1. 通过键值对的形式为注解属性赋值
  2. 编译器检查注解的使用范围,并将注解信息写入元素属性表
  3. 运行时JVM将RUNTIME的所有注解属性取出并最终存入map里
  4. 创建 AnnotationInvocationHandler 实例并传入前面的map
  5. JVM 使用 JDK 动态代理为注解生成代理类,并初始化处理器
  6. 调用 invoke 方法, 通过传入方法名返回注解对应的属性值

1. 通过键值对的形式为注解属性赋值

@CourseInfoAnnotation(
        courseName = "英语",
        courseTag = "必修",
        courseProfile = "英语模块"
)
public class MyCourse {
}

2. 编译器检查注解的使用范围,并将注解信息写入元素属性表

首先编译器检查注解修饰的范围是否正确,否则无法通过编译。

通过编译后,用工具查看编译后的字节码文件,带有注解信息

image.png

3. 运行时JVM将RUNTIME的所有注解属性取出并最终存入map里
JVM 会将单个 Class 文件中生命周期为 RUNTIME 的注解取出并放入map中

4. 创建 AnnotationInvocationHandler 实例并传入前面的map

private Annotation generateAnnotation() {
    return AnnotationParser.annotationForMap(annoType,
                                             getAllReflectedValues());
}
public static Annotation annotationForMap(final Class<? extends Annotation> type,
                                          final Map<String, Object> memberValues)
{
    return AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
        public Annotation run() {
            return (Annotation) Proxy.newProxyInstance(
                type.getClassLoader(), new Class<?>[] { type },
                new AnnotationInvocationHandler(type, memberValues));
        }});
}

5. JVM 使用 JDK 动态代理为注解生成代理类,并初始化处理器

image.png

初始化处理器,调用 $Proxy 的构造函数,执行父类构造函数,将 InvocationHandler 初始化

public $Proxy0(InvocationHandler var1) throws  {
    super(var1);
}
protected Proxy(InvocationHandler h) {
    Objects.requireNonNull(h);
    this.h = h;
}

6. 调用 invoke 方法, 通过传入方法名返回注解对应的属性值

版权声明:本文为油虾条的博客原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/aweicode/p/16855023.html