likes
comments
collection
share

深入理解Java注解的实现原理,注解的本质

作者站长头像
站长
· 阅读数 14

注解🚩

注解也被称为元数据

这个名字也体现了注解的价值:在某处提供额外的信息,便于之后使用这些信息

注解有多重要

以前的框架流行的是xml配置,而现在更多的是用注解。主流的Spring开发都是全注解开发。

自定义注解最常见的应用场景就是:Spring AOP,用来做日志切面打印处理

因此,学会元注解,自定义注解,了解注解实现原理是Java程序员的必修课

内置注解

java.lang提供的基础注解:

  • @Deprecated:表示代码被弃用
  • @SuppressWarnings:表示关闭编译器警告信息 有参数,直接用(all)吧
  • @Override:表示方法被覆写

JAVA8新增:

@FunctionalInterface:表示一个函数式接口

元注解

元注解: 是针对 public @interface Annotation {} 自己实现注解时用到的基础注解

  1. @targert 表示可以修饰什么内容
  2. @Retention & @RetentionTarget 表示注解在它所修饰的类中可以被保留到何时,注解的生命周期
  3. @Inherited 表示被该注解修饰的类 的子类 会一起继承该注解
  4. @Documented:注解是否应当被包含在 JavaDoc 文档中
@target取值:

注:可以用{}多选

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD:允许作用在属性字段上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造器上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上
@Retention取值:
  • RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
  • RetentionPolicy.CLASS:类加载阶段丢弃,在class文件的属性表中用 RuntimeInvisibleAnnotations表示
  • RetentionPolicy.RUNTIME:永久保存,可以反射获取,一般自定义注解都是RUNTIME,在class文件的属性表中用RuntimeVisibleAnnotations 表示
Java8新增的元注解

@Repeatable :使用这个注解时,可以多次修饰

@Native :注解修饰成员变量:表示这个变量可以被本地代码引用,不常用

注解与继承

定义注解时无法继承注解。毕竟编写注解不会花费你太多时间,更多的是元注解的定义和一张哈希表。与继承强调的代码复用只能说没什么关系。

我们的@Inherited 元注解,是指:一个父类的被@Inherited 修饰的注解,子类也会有。这两点需要区别开来

自定义注解

  1. 定义注解
  @Target(ElementType.METHOD)
  @Retention(RetentionPolicy.RUNTIME)
 public @interface MyMethodAnnotation {
     public String title() default "";
     public String description() default "";
 }
  1. 使用注解
@MyMethodAnnotation(title = "xxx", description = "xxx")
     public String xxx() {
         return "";
     }
  1. 获取注解
  public static void main(String[] args) {
      try {
          // 获取所有methods
          Method[] methods = TestMethodAnnotation.class.getClassLoader()
                  .loadClass(("com.pdai.java.annotation.TestMethodAnnotation"))
                  .getMethods();
  
          // 遍历
          for (Method method : methods) {
             // 方法上是否有MyMethodAnnotation注解
             if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
                     // 获取MyMethodAnnotation对象信息
                     MyMethodAnnotation methodAnno = method
                             .getAnnotation(MyMethodAnnotation.class);
                  // 访问注解的属性
                     System.out.println(methodAnno.title());
             }
         }

注解如何生效

  • 编译器扫描处理
  • 运行期反射处理

编译器扫描处理一般只有Java内置注解会用到,比如@Override修饰的方法,编译器会检查父类是否有相同的方法

而大部分自定义的注解,都是在运行期通过反射拿到并处理。

运行时注解存放在哪里

在class文件中的attributes属性表中。

运行期如何获取注解🚩

反射获取注解的核心在: java.lang.reflect下的 AnnotatedElement接口,而AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口。

对于一个类或者接口来说,Class 类中提供了以下一些方法用于注解操作。

判断

判断是否包含指定类型的注解

boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
// 此方法会忽略注解对应的注解容器

获取

1、获取指定类型的注解

 <T extends Annotation> T getAnnotation(Class<T> annotationClass) 
     // xxx name = getAnnotation(xxx.class);
     // 若不存在 返回null

如果该注解可重复,即同type的注解有多个:

<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) 
    //若不存在 返回长度为0的数组

忽略继承的注解的版本 + Declared

<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) 
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) 

2、获取所有注解

Annotation[] getAnnotations() 
// 若没有注解  返回长度为0的数组

同样有个Declared的版本忽略继承的注解

Annotation[] getDeclaredAnnotations() 

注解实现原理/本质

此处是面向运行期注解的实现原理,在此关于编译期注解简单说一嘴

JDK5首次提出注解仅仅面向运行期注解,在JDK6才提出了编译期注解,提供了「插入式注解处理器」的API,这会影响前端编译器的工作。比如:Lombok,这个够有名吧,就是利用了「插入式注解处理器」实现的功能。

在Javac源码中,插入式注解处理器的初始化过程是在initPorcessAnnotations()方法中完成的,而它的执行过程则是在processAnnotations()方法中完成。这个方法会判断是否还有新的注解处理器需要执行,如果有的话,通过com.sun.tools.javac.processing.JavacProcessing-Environment类的doProcessing()方法来生成一个新的JavaCompiler对象,对编译的后续步骤进行处理。

推荐阅读:周志明《深入理解Java虚拟机》P510:插入式注解处理器实战

Java注解处理器这篇文章详细分析了编译期注解的原理,很推荐

注解的本质就是一个继承了 Annotation 接口的接口,因此也会被编译成class文件

public interface Override extends Annotation{
    
}

没错,注解本身就是一个接口

  public String test() default "";

并且注解内部的“数据”,本质是一个接口方法。

但我们是可以通过反射拿到Annotation实例的,那么:

  • Annotation明明是个接口,怎么实例化的?
  • 方法也是抽象方法,它的执行逻辑去哪里了

实际上,我们在运行期获取到的注解,都是代理类。

public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
    return (A) annotationData().annotations.get(annotationClass);
}

最终调用到

public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) {
    return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
        public Annotation run() {
            // 这不是 老朋友 JDK动态代理 吗
            return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), 
               new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
        }
    });
}

Proxy.newProxyInstance,这个东西眼熟吧。

JDK动态代理核心:AnnotationInvocationHandler

核心属性:

private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;

注意上面调用的构造函数:

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        this.type = var1;
        this.memberValues = var2;
}   // 省略注解类的判断

重点看invoke方法

public Object invoke(Object var1, Method var2, Object[] var3) {
    // 方法名
    String var4 = var2.getName();
    // 此处省略 一些不需要代理的方法比如equals,hashcode,toString 以及参数校验
    // 像annotationType方法,也是不代理 直接返回注解的type
    if (var4.equals("annotationType")  return this.type;
    // 这里是正常的代理逻辑:
        // 可以看到 对于接口的方法 转化为了对memberValues的get操作
            Object var6 = this.memberValues.get(var4);
            if(异常){抛出} 
            else {
                // 如果是数组的话 会拷贝一份 对拿到的数组的修改 不会影响到注解本身的值
                if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                    var6 = this.cloneArray(var6);
                }
                return var6;
            }
        }
    }
}

注解原理小结

注解本质是个接口,无法实例化,所以我们在运行期反射拿到的注解,其实是Proxy代理对象,本质是JDK动态代理。核心的代理类是AnnotationInvocationHandler。这个类内部用一张Map存储注解的k - v,用一个Class描述注解的类型。对注解内数据的获取,因为注解是个接口,方法都是抽象方法,实际仅仅是对内部的map的get调用。当然此处会屏蔽一些比如toString,equals等不需要代理的方法。

再往JVM底层说,注解也被存储在class文件的属性表,包括了注解的全类名,以及若干pair键值对,保存注解的参数和具体值。

参考文献

Java 基础 - 注解机制详解

Java 注解机制

java注解的本质以及注解的底层实现原理

转载自:https://juejin.cn/post/7275943355879112743
评论
请登录