JAVA 注解的基本原理

之前,『XML』是各大框架的青睐者,它以松耦合的方式完成了框架中几乎全部的配置,可是随着项目愈来愈庞大,『XML』的内容也愈来愈复杂,维护成本变高。java

因而就有人提出来一种标记式高耦合的配置方式,『注解』。方法上能够进行注解,类上也能够注解,字段属性上也能够注解,反正几乎须要配置的地方均可以进行注解。git

关于『注解』和『XML』两种不一样的配置模式,争论了好多年了,各有各的优劣,注解能够提供更大的便捷性,易于维护修改,但耦合度高,而 XML 相对于注解则是相反的。github

追求低耦合就要抛弃高效率,追求效率必然会遇到耦合。本文意再也不辨析二者谁优谁劣,而在于以最简单的语言介绍注解相关的基本内容。bash

注解的本质

「java.lang.annotation.Annotation」接口中有这么一句话,用来描述『注解』。微信

The common interface extended by all annotation types框架

全部的注解类型都继承自这个普通的接口(Annotation)ide

这句话有点抽象,但却说出了注解的本质。咱们看一个 JDK 内置注解的定义:函数

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}
复制代码

这是注解 @Override 的定义,其实它本质上就是:spa

public interface Override extends Annotation{
    
}
复制代码

没错,注解的本质就是一个继承了 Annotation 接口的接口。有关这一点,你能够去反编译任意一个注解类,你会获得结果的。设计

一个注解准确意义上来讲,只不过是一种特殊的注释而已,若是没有解析它的代码,它可能连注释都不如。

而解析一个类或者方法的注解每每有两种形式,一种是编译期直接的扫描,一种是运行期反射。反射的事情咱们待会说,而编译器的扫描指的是编译器在对 java 代码编译字节码的过程当中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。

典型的就是注解 @Override,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具备一个一样的方法签名。

这一种状况只适用于那些编译器已经熟知的注解类,好比 JDK 内置的几个注解,而你自定义的注解,编译器是不知道你这个注解的做用的,固然也不知道该如何处理,每每只是会根据该注解的做用范围来选择是否编译进字节码文件,仅此而已。

元注解

『元注解』是用于修饰注解的注解,一般用在注解的定义上,例如:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}
复制代码

这是咱们 @Override 注解的定义,你能够看到其中的 @Target,@Retention 两个注解就是咱们所谓的『元注解』,『元注解』通常用于指定某个注解生命周期以及做用目标等信息。

JAVA 中有如下几个『元注解』:

  • @Target:注解的做用目标
  • @Retention:注解的生命周期
  • @Documented:注解是否应当被包含在 JavaDoc 文档中
  • @Inherited:是否容许子类继承该注解

其中,@Target 用于指明被修饰的注解最终能够做用的目标是谁,也就是指明,你的注解究竟是用来修饰方法的?修饰类的?仍是用来修饰字段属性的。

@Target 的定义以下:

image

咱们能够经过如下的方式来为这个 value 传值:

@Target(value = {ElementType.FIELD})
复制代码

被这个 @Target 注解修饰的注解将只能做用在成员字段上,不能用于修饰方法或者类。其中,ElementType 是一个枚举类型,有如下一些值:

  • ElementType.TYPE:容许被修饰的注解做用在类、接口和枚举上
  • ElementType.FIELD:容许做用在属性字段上
  • ElementType.METHOD:容许做用在方法上
  • ElementType.PARAMETER:容许做用在方法参数上
  • ElementType.CONSTRUCTOR:容许做用在构造器上
  • ElementType.LOCAL_VARIABLE:容许做用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:容许做用在注解上
  • ElementType.PACKAGE:容许做用在包上

@Retention 用于指明当前注解的生命周期,它的基本定义以下:

image

一样的,它也有一个 value 属性:

@Retention(value = RetentionPolicy.RUNTIME
复制代码

这里的 RetentionPolicy 依然是一个枚举类型,它有如下几个枚举值可取:

  • RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
  • RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
  • RetentionPolicy.RUNTIME:永久保存,能够反射获取

@Retention 注解指定了被修饰的注解的生命周期,一种是只能在编译期可见,编译后会被丢弃,一种会被编译器编译进 class 文件中,不管是类或是方法,乃至字段,他们都是有属性表的,而 JAVA 虚拟机也定义了几种注解属性表用于存储注解信息,可是这种可见性不能带到方法区,类加载时会予以丢弃,最后一种则是永久存在的可见性。

剩下两种类型的注解咱们平常用的很少,也比较简单,这里再也不详细的进行介绍了,你只须要知道他们各自的做用便可。@Documented 注解修饰的注解,当咱们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。@Inherited 注解修饰的注解是具备可继承性的,也就说咱们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。

JAVA 的内置三大注解

除了上述四种元注解外,JDK 还为咱们预约义了另外三种注解,它们是:

  • @Override
  • @Deprecated
  • @SuppressWarnings

@Override 注解想必是你们很熟悉的了,它的定义以下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
复制代码

它没有任何的属性,因此并不能存储任何其余信息。它只能做用于方法之上,编译结束后将被丢弃。

因此你看,它就是一种典型的『标记式注解』,仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程当中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具备一个一样方法签名的函数,若是不是,天然不能经过编译。

@Deprecated 的基本定义以下:

image

依然是一种『标记式注解』,永久存在,能够修饰全部的类型,做用是,标记当前的类或者方法或者字段等已经再也不被推荐使用了,可能下一次的 JDK 版本就会删除。

固然,编译器并不会强制要求你作什么,只是告诉你 JDK 已经再也不推荐使用当前的方法或者类了,建议你使用某个替代者。

@SuppressWarnings 主要用来压制 java 的警告,它的基本定义以下:

image

它有一个 value 属性须要你主动的传值,这个 value 表明一个什么意思呢,这个 value 表明的就是须要被压制的警告类型。例如:

public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}
复制代码

这么一段代码,程序启动时编译器会报一个警告。

Warning:(8, 21) java: java.util.Date 中的 Date(int,int,int) 已过期

而若是咱们不但愿程序启动时,编译器检查代码中过期的方法,就可使用 @SuppressWarnings 注解并给它的 value 属性传入一个参数值来压制编译器的检查。

@SuppressWarning(value = "deprecated")
public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}
复制代码

这样你就会发现,编译器再也不检查 main 方法下是否有过期的方法调用,也就压制了编译器对于这种警告的检查。

固然,JAVA 中还有不少的警告类型,他们都会对应一个字符串,经过设置 value 属性的值便可压制对于这一类警告类型的检查。

自定义注解的相关内容就再也不赘述了,比较简单,经过相似如下的语法便可自定义一个注解。

public @interface InnotationName{
    
}
复制代码

固然,自定义注解的时候也能够选择性的使用元注解进行修饰,这样你能够更加具体的指定你的注解的生命周期、做用范围等信息。

注解与反射

上述内容咱们介绍了注解使用上的细节,也简单提到,「注解的本质就是一个继承了 Annotation 接口的接口」,如今咱们就来从虚拟机的层面看看,注解的本质究竟是什么。

首先,咱们自定义一个注解类型:

image

这里咱们指定了 Hello 这个注解只能修饰字段和方法,而且该注解永久存活,以便咱们反射获取。

以前咱们说过,虚拟机规范定义了一系列和注解相关的属性表,也就是说,不管是字段、方法或是类自己,若是被注解修饰了,就能够被写进字节码文件。属性表有如下几种:

  • RuntimeVisibleAnnotations:运行时可见的注解
  • RuntimeInVisibleAnnotations:运行时不可见的注解
  • RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
  • RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
  • AnnotationDefault:注解类元素的默认值

给你们看虚拟机的这几个注解相关的属性表的目的在于,让你们从总体上构建一个基本的印象,注解在字节码文件中是如何存储的。

因此,对于一个类或者接口来讲,Class 类中提供了如下一些方法用于反射注解。

  • getAnnotation:返回指定的注解
  • isAnnotationPresent:断定当前元素是否被指定注解修饰
  • getAnnotations:返回全部的注解
  • getDeclaredAnnotation:返回本元素的指定注解
  • getDeclaredAnnotations:返回本元素的全部注解,不包含父类继承而来的

方法、字段中相关反射注解的方法基本是相似的,这里再也不赘述,咱们下面看一个完整的例子。

首先,设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

而后 main 函数。

image

咱们说过,注解本质上是继承了 Annotation 接口的接口,而当你经过反射,也就是咱们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是经过动态代理机制生成一个实现咱们注解(接口)的代理类。

咱们运行程序后,会看到输出目录里有这么一个代理类,反编译以后是这样的:

image

image

代理类实现接口 Hello 并重写其全部方法,包括 value 方法以及接口 Hello 从 Annotation 接口继承而来的方法。

而这个关键的 InvocationHandler 实例是谁?

AnnotationInvocationHandler 是 JAVA 中专门用于处理注解的 Handler, 这个类的设计也很是有意思。

image

这里有一个 memberValues,它是一个 Map 键值对,键是咱们注解属性名称,值就是该属性当初被赋上的值。

image

image

而这个 invoke 方法就颇有意思了,你们注意看,咱们的代理类代理了 Hello 接口中全部的方法,因此对于代理类中任何方法的调用都会被转到这里来。

var2 指向被调用的方法实例,而这里首先用变量 var4 获取该方法的简明名称,接着 switch 结构判断当前的调用方法是谁,若是是 Annotation 中的四大方法,将 var7 赋上特定的值。

若是当前调用的方法是 toString,equals,hashCode,annotationType 的话,AnnotationInvocationHandler 实例中已经预约义好了这些方法的实现,直接调用便可。

那么假如 var7 没有匹配上这四种方法,说明当前的方法调用的是自定义注解字节声明的方法,例如咱们 Hello 注解的 value 方法。这种状况下,将从咱们的注解 map 中获取这个注解属性对应的值。

其实,JAVA 中的注解设计我的以为有点反人类,明明是属性的操做,非要用方法来实现。固然,若是你有不一样的看法,欢迎留言探讨。

最后咱们再总结一下整个反射注解的工做原理:

首先,咱们经过键值对的形式能够为注解属性赋值,像这样:@Hello(value = "hello")。

接着,你用注解修饰某个元素,编译器将在编译期扫描每一个类或者方法上的注解,会作一个基本的检查,你的这个注解是否容许做用在当前位置,最后会将注解信息写入元素的属性表。

而后,当你进行反射的时候,虚拟机将全部生命周期在 RUNTIME 的注解取出来放到一个 map 中,并建立一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。

最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。

那么这样,一个注解的实例就建立出来了,它本质上就是一个代理类,你应当去理解好 AnnotationInvocationHandler 中 invoke 方法的实现逻辑,这是核心。一句话归纳就是,经过方法名返回注解属性值


文章中的全部代码、图片、文件都云存储在个人 GitHub 上:

(https://github.com/SingleYam/overview_java)

欢迎关注微信公众号:OneJavaCoder,全部文章都将同步在公众号上。

image
相关文章
相关标签/搜索