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

注解🚩

注解也被称为元数据

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

注解有多重要

以前的框架流行的是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键值对,保存注解的参数和具体值。

参考文献

  1. Java 基础 - 注解机制详解

  2. Java 注解机制

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


相关推荐

  • 当自动驾驶遇上GPT-4V:L4要解决了?
  • 超级干货 | 数据平滑9大妙招
  • 【深度学习】计算机视觉领域如何从别人的论文里获取自己的idea?
  • 保姆级教程:不到30行代码上手讯飞版ChatGPT-API
  • 巧用 Redis,实现微博 Feed 流功能!
  • 微软全力拥抱 Java !
  • 使用CSS圆锥渐变创建背景图案
  • 让 web 再次伟大:用 CanvasKit 实现超级丝滑的原神地图(已开源)!!!
  • 不满意网上的Token无感知刷新方案,自己琢磨了个解决方案~
  • 当个 PM 式程序员「GitHub 热点速览」
  • 「原生案例」如何在JavaScript中实现实时搜索功能
  • 基于开源项目或云产品构建属于自己的私域知识库问答系统
  • 字节跳动李航:对语言大模型的若干观察和思考
  • 还有这操作?这都什么公司?
  • [开源]高性能、高吞吐量、高扩展性物联网平台,单机支持百万链接
  • 时序预测的王道 -- Patch 。
  • 谷歌、AMD、英特尔加入挑战,英伟达AI解决方案还能继续“遥遥领先”吗?
  • 推理成本增加10倍?对文心大模型4.0的一些猜想
  • 微服务是个坏主意吗?
  • 博士申请 | 香港科技大学郭毅可院士团队招收生成式AI方向博士/博后/RA