Java注解

189 阅读3分钟

注解


  1. 使用@interface语法来定义注解(Annotation)
  2. Java 中所有注解默认实现 Annotation 接口
  3. 注解是元数据的一种形式,提供有关于程序但不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。
  4. 注解的参数类似无参数方法,可以用default设定一个默认值(强烈推荐)。最常用的参数应当命名为value
public interface Annotation {
    boolean equals(Object obj); 
    int hashCode(); 
    String toString();
    Class<? extends Annotation> annotationType();
}

public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

元注解

有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)常用:

@Target

使用@Target可以定义Annotation能够被应用于源码的哪些位置:

  • ElementType.ANNOTATION_TYPE 注解类型。
  • ElementType.CONSTRUCTOR 构造函数。
  • ElementType.FIELD 字段或属性。
  • ElementType.LOCAL_VARIABLE 局部变量。
  • ElementType.METHOD 方法级注解。
  • ElementType.PACKAGE 包声明。
  • ElementType.PARAMETER 方法的参数。
  • ElementType.TYPE 类的任何元素。

@Retention

元注解@Retention定义了Annotation的生命周期,默认为CLASS

  • RetentionPolicy.SOURCE - 标记的注解仅保留在源级别中,并被编译器忽略
  • RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
  • RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留,因此运行时环境可以使用它。
级别技术使用场景
源码APT在编译期能够获取注解与注解声明的类包括类中所有成员信息,一般用于生成额外的辅助类。
字节码字节码增强在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
运行时反射在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定。
APT注解处理器

APT全称为:Anotation Processor Tools,意为注解处理器,用于处理注解。编写好的Java源文件,需要经过 javac 的编译,翻译为虚拟机能够加载解析的字节码Class文件。注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。 注册的注解处理器由 javac调起,并将注解信息传递给注解处理器进行处理。

注解处理器可以增删改抽象语法树的任意元素。执行到注解处理器,都会重新执行词法分析、语法分析、填充符号表步,直到注解处理器不再对语法树进行修改为止,每一次的循环过程都称为一次Round。

注解处理器是对注解应用最为广泛的场景。在GlideEventBus3ButterkniferTinkerARouter等常用框架中都有注解处理器的身影。这些框架中对注解的定义并不是 SOURCE 级别,更多的是 CLASS 级别 SOURCE < CLASS < RUNTIME

image-20220720092520019

字节码增强

字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态。此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成或修改符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

反射

反射就是Reflection,反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。是Java被视为动态语言的关键。

反射是可以修改final变量的,但是如果是基本数据类型或者String类型的时候,无法通过对象获取修改后的值,因为JVM对其进行了内联优化 内联函数,编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支

image-20220720095640463

反射慢的原因

  • Method#invoke 需要进行自动拆装箱

    invoke 方法的参数是 Object[] 类型,基本数据类型会转化为Integer装箱,同时再包装成Object数组。在执行时候又会把数组拆解开,并拆箱为基本数据类型

  • 反射需要按名检索类和方法

  • 反射时需要检查方法可见性以及每个实际参数与形式参数的类型匹配性

  • 编译器无法对动态调用的代码做优化

动态代理原理

Java标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例

动态代理会在运行时再创建代理类和其实例,因此显然效率较低。

//JDK动态代理: 
 public static Object newProxyInstance(@RecentlyNullable ClassLoader loader,//类加载器去加载代理对象
                                       @RecentlyNonNull Class<?>[] interfaces, //动态代理类需要实现的接口
                                       @RecentlyNonNull InvocationHandler h  //动态代理方法在执行时,会调用h里面的invoke方法去执行
                                      ) throws IllegalArgumentException {
        throw new RuntimeException("Stub!");
    }

Proxy.newProxyInstance(getClass().getClassLoader(),
                       new Class[]{Api.class},  
                       new InvocationHandler() {
                           @Override 
                           public Object invoke(Object proxy,//就是代理对象,newProxyInstance方法的返回对象
                                                Method method, //method:调用的方法
                                                Object[] args//args: 方法中的参数
                                               )throws Throwable { //执行真实对象方法 
                               return method.invoke(api, args); 
                           } 
                       });