你真的懂注解吗?深入理解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 {
          
          Method[] methods = TestMethodAnnotation.class.getClassLoader()
                  .loadClass(("com.pdai.java.annotation.TestMethodAnnotation"))
                  .getMethods();
  
          
          for (Method method : methods) {
             
             if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
                     
                     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) 
     
     

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

<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) 
    

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

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

2、获取所有注解

Annotation[] getAnnotations() 


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

Annotation[] getDeclaredAnnotations() 

注解实现原理/本质

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

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

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

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

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

注解的本质就是一个继承了 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() {
            
            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();
    
    
    if (var4.equals("annotationType")  return this.type;
    
        
            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 基础 - 注解机制详解[2]

Java 注解机制[3]

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

参考资料

[1]

https://www.race604.com/annotation-processing/: https://link.juejin.cn/?target=https%3A%2F%2Fwww.race604.com%2Fannotation-processing%2F

[2]

https://www.pdai.tech/md/java/basic/java-basic-x-annotation.html: https://link.juejin.cn/?target=https%3A%2F%2Fwww.pdai.tech%2Fmd%2Fjava%2Fbasic%2Fjava-basic-x-annotation.html

[3]

https://blog.csdn.net/m0_37235955/article/details/115678621?spm=1001.2014.3001.5501: https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fm0_37235955%2Farticle%2Fdetails%2F115678621%3Fspm%3D1001.2014.3001.5501

[4]

https://blog.csdn.net/qq_20009015/article/details/106038023: https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fqq_20009015%2Farticle%2Fdetails%2F106038023

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java注解(Annotation)是一种元数据,用于在程序中提供额外的信息但并不直接参与程序的运行。它们被用来标记代码元素,如类、方法、变量等,以便工具和构建系统能够读取这些信息并执行特定的行为。Java注解不改变源代码的实际功能,而是提供一种声明式的方式来传递上下文或配置。 注解的工作原理主要包括以下几个步骤: 1. **编写注解**:程序员在源代码中定义注解,如`@Override`、`@Deprecated`等,这些是Java标准库提供的预定义注解。 2. **编译期间处理**:当源代码经过Java编译器(javac)编译时,注解会被保留下来,但不会影响字节码。编译器会在注解的位置生成对应的字节码,包含注解的数据。 3. **解析注解**:使用诸如AspectJ、IntelliJ IDEA这样的工具或框架,在运行时或构建过程中读取和解析注解信息。例如,IDEA可以显示方法是否实现了某个接口的注解信息。 4. **自定义处理器**:开发者可以通过编写注解处理器(Annotation Processor),在编译时动态地处理注解,比如自动生成代码、验证约束等。 至于反编译,Java字节码(.class文件)是可以被反编译成源代码的,这通常用于调试、逆向工程或研究。有许多开源工具可供选择,如JD-GUI、Procyon或 FernFlower等。反编译后得到的源代码可能会失去一些注解相关的元数据,因为注解主要为编译器和特定工具设计,并非为人类阅读优化的文本形式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术小羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值