学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Java注解

Spring Boot框架里面用到了很多注解,发现不明所以,所以学习一下。

注解简介

出现注解之前,XML大行其道,以松耦合的方式完成了框架中几乎所有的配置,但是随着项目越来越大,xml的内容也越来越复杂,维护成本变高。

于是一种标记式高耦合的配置方式产生了,注解,可以作用于类,方法,属性,几乎需要配置的地方都可以进行注解。

xml和注解两种不同配置方式,各有优劣,追求低耦合就要抛弃高效率,追求效率必然会遇到耦合

注解的本质

The common interface extended by all annotation types
所有的注解类型都继承自这个普通的接口(Annotation)

eg:

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

其实本质上就是

1
2
3
public interface Override extends Annotation{

}

注解的本质就是一个继承了 Annotation 接口的接口

注解的分类

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

解析一个类或者方法的注解往往有两种形式,一种是编译器直接的扫描,一种是运行期反射

编译器注解

编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理,典型的就是注解 @Override。这一种情况只适用于那些编译器已经熟知的注解类,比如 JDK 内置的几个注解。

  1. @Override
  2. @Deprecated
  3. @SuppressWarnings

元注解

元注解是用于修饰注解的注解,通常用在注解的定义上,一般用于指定某个注解生命周期以及作用目标等信息

eg:

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

JAVA 中有以下几个元注解:

  1. @Target:注解的作用目标
  2. @Retention:注解的生命周期
  3. @Documented:注解是否应当被包含在 JavaDoc 文档中
  4. @Inherited:是否允许子类继承该注解

@Target

@Target 的定义如下:

可以通过以下的方式来为这个 value 传值:
@Target(value = {ElementType.FIELD})

ElementType 是一个枚举类型,有以下一些值:

  1. ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  2. ElementType.FIELD:允许作用在属性字段上
  3. ElementType.METHOD:允许作用在方法上
  4. ElementType.PARAMETER:允许作用在方法参数上
  5. ElementType.CONSTRUCTOR:允许作用在构造器上
  6. ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  7. ElementType.ANNOTATION_TYPE:允许作用在注解上
  8. ElementType.PACKAGE:允许作用在包上

@Retention

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

也有一个 value 属性:
@Retention(value = RetentionPolicy.RUNTIME
RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:

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

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

@Documented

@Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。@Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。

注解与反射

从虚拟机的层面看看,注解的本质

自定义一个注解类型:

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

虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。

属性表有以下几种:

  1. RuntimeVisibleAnnotations:运行时可见的注解
  2. RuntimeInVisibleAnnotations:运行时不可见的注解
  3. RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
  4. RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
  5. AnnotationDefault:注解类元素的默认值

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

  1. getAnnotation:返回指定的注解
  2. isAnnotationPresent:判定当前元素是否被指定注解修饰
  3. getAnnotations:返回所有的注解
  4. getDeclaredAnnotation:返回本元素的指定注解
  5. getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的

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

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

然后 main 函数

注解本质上是继承了 Annotation 接口的接口,而当你通过反射,也就是我们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。
运行程序后,会看到输出目录里有这么一个代理类,反编译之后是这样的:

代理类实现接口 Hello 并重写其所有方法,包括 value 方法以及接口 Hello 从 Annotation 接口继承而来的方法。而这个关键的 InvocationHandler 实例是谁?
AnnotationInvocationHandler 是 JAVA 中专门用于处理注解的 Handler

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

而这个 invoke 方法就很有意思了,大家注意看,我们的代理类代理了 Hello 接口中所有的方法,所以对于代理类中任何方法的调用都会被转到这里来。

var2 指向被调用的方法实例,而这里首先用变量 var4 获取该方法的简明名称,接着 switch 结构判断当前的调用方法是谁,如果是 Annotation 中的四大方法,将 var7 赋上特定的值。
如果当前调用的方法是 toString,equals,hashCode,annotationType 的话,AnnotationInvocationHandler 实例中已经预定义好了这些方法的实现,直接调用即可。
那么假如 var7 没有匹配上这四种方法,说明当前的方法调用的是自定义注解字节声明的方法,例如我们 Hello 注解的 value 方法。这种情况下,将从我们的注解 map 中获取这个注解属性对应的值。

反射注解的工作原理:

  1. 通过键值对的形式为注解属性赋值,像这样:@Hello(value = “hello”)。
  2. 用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
  3. 当进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。
  4. 最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。这样,一个注解的实例就创建出来了,它本质上就是一个代理类,你应当去理解好 AnnotationInvocationHandler 中 invoke 方法的实现逻辑,这是核心。一句话概括就是,通过方法名返回注解属性值

参考

  1. JAVA 注解的基本原理
  2. Java自定义注解