[关闭]
@Otokaze 2018-11-30T05:48:49.000000Z 字数 6044 阅读 585

Java 注解原理

Java

反射相关 API

java.lang.Class

  1. // 是否为注解类型
  2. boolean isAnnotation();
  3. // 是否存在指定注解
  4. boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
  5. // 包括继承的注解
  6. Annotation[] getAnnotations();
  7. <A extends Annotation> A getAnnotation(Class<A> annotationClass);
  8. // 忽略继承的注解
  9. Annotation[] getDeclaredAnnotations();
  10. <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass);

java.lang.reflect.Field

  1. // 包括继承的注解
  2. Annotation[] getAnnotations();
  3. <A extends Annotation> A getAnnotation(Class<A> annotationClass);
  4. // 忽略继承的注解
  5. Annotation[] getDeclaredAnnotations();
  6. <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass);

java.lang.reflect.Method

  1. // 包括继承的注解
  2. Annotation[] getAnnotations();
  3. <A extends Annotation> A getAnnotation(Class<A> annotationClass);
  4. // 忽略继承的注解
  5. Annotation[] getDeclaredAnnotations();
  6. <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass);
  7. // 获取参数的注解
  8. Annotation[][] getParameterAnnotations();

java.lang.reflect.Constructor

  1. // 包括继承的注解
  2. Annotation[] getAnnotations();
  3. <A extends Annotation> A getAnnotation(Class<A> annotationClass);
  4. // 忽略继承的注解
  5. Annotation[] getDeclaredAnnotations();
  6. <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass);
  7. // 获取参数的注解
  8. Annotation[][] getParameterAnnotations();

常用的反射 API 类有:Class、Field、Method、Constructor。这些类都是对应的 类、字段、方法、构造器 的抽象表示,或者说是他们的元数据对象。比如一个 Class 对象就表示一个“类”,一个 Field 对象就表示一个“成员变量”,一个 Method 对象就表示一个“成员方法”,一个 Constructor 对象就表示一个“构造函数”。

注解基本知识

Java 注解相关的 API 存放在 java.lang.annotation 包。

注解是 Java 5 引入的新特性,注解可以看作是一种注释,注解的通常作用是用来传递元数据。注解是 Java 中的一等公民,是一个类型,比如类、抽象类、接口、枚举都是一等公民,而注解也是一等公民,它们都是 Java 中的“类型”。

注解的标识符由一个 @ 符号开头,后接一个 Java 标识符,如 @MyAnnotation。注解可以包含元素,所谓元素可以理解为参数/选项,比如 @MyAnnotation(name = "Otokaze", age = 18) 注解包含两个元素,name 和 age,这些数据又被称为元数据,我们可以通过 注解处理器Java 反射 API 获取到这些元数据,从而执行某些操作。

注解可以标注的元素:包、类、接口、枚举、注解、构造方法、成员方法、静态方法、方法参数、成员变量、局部变量等。虽然注解可以标注这么多东西,但是最常见的注解会标注在 类型(类、接口、枚举)成员变量成员方法构造方法静态方法方法参数。其实说简单一点就是标注在这四种位置:类型、方法、字段、参数。

元注解:所谓元注解就是用来标注 注解 的注解类型,可类比为数据和元数据。常用的几个:

Java 内置注解,最常用的就是 @Override 了,它是一个源码注解,javac 编译器在检测到 @Override 标注的方法时会检查该方法是否正确重写了它的父类的相同签名的方法,如果没有则抛出编译错误,否则将忽略。

如何创建自己的注解:很简单,和创建接口相似,来看一个简单的例子:

  1. public @interface MyAnnotation {
  2. String name() default "name";
  3. String value() default "value";
  4. }

创建注解的关键字是 @interface,即 @ 符号加 interface 接口定义符,不要感到奇怪,为什么不创建一个新的关键字来定义注解,而是使用 @interface,这里面其实是一个强烈的暗示,注解和接口有什么关联?

注解的元素其实就是“接口中的方法”,比如上面就定义了两个接口方法:

  1. String name();
  2. String value();

不过我们还定义了元素的默认值,写法是直接在元素定义的后面加上 default 关键字。

如何使用注解就不多说了,相信各位都很熟悉。我们来分析一下注解与接口的关系。前面我们说了,注解与接口很相似,相似到它们的关键字都是一样的,一个是 interface 一个是 @interface。之所以名字相同不是没有原因的。我们可以使用 jdk 自带的 javap 反编译工具来查看注解 MyAnnotation 究竟是什么东西:

  1. $ javap MyAnnotation.class
  2. Compiled from "MyAnnotation.java"
  3. public interface com.zfl9.MyAnnotation extends java.lang.annotation.Annotation {
  4. public abstract java.lang.String name();
  5. public abstract java.lang.String value();
  6. }

看出什么了吗?没错,注解其实就是接口,所谓的注解类型其实就是继承自 java.lang.annotation.Annotation 接口的子接口,我们来看看 Annotation 接口的定义:

  1. package java.lang.annotation;
  2. public interface Annotation {
  3. boolean equals(Object obj);
  4. int hashCode();
  5. String toString();
  6. Class<? extends Annotation> annotationType();
  7. }

我们忽略掉 Object 上的几个基类方法,Annotation 上只定义了一个 Class annotationType() 方法,用来获取当前注解实例对应的注解 Class 对象。好吧,这些其实都不重要,我们只需要知道一点,注解是一个继承自 Annotation 接口的子接口。记住这一点!!!

还有一个有用的知识就是,如果注解只有一个元素,建议把这个元素的名称改为 value,因为当我们给注解的元素赋值时,如果没有指明元素名,那么 Java 默认假设这个元素名为 value,设为 value 是一个最佳实践。

元素的类型

所谓的元素类型其实就是抽象方法的返回值类型!

1) 八大基本类型(boolean、byte、char、short、int、long、float、double)
2) String 字符串
3) Class 类
4) enum 枚举
5) Annotation 注解
6) 以上所有类型的数组

使用其它类型会抛出一个编译错误。

注解底层原理

注解可以粗略的分为两种,一种是 编译期注解,一种是 运行期注解。前面说了,注解主要起到的是一个传递元数据的作用,在注解出现之前,我们也有其他传递元数据的方式,比如 Java 中广泛使用的 XML 配置文件。XML 和注解的作用基本上是相同的,都是用来起到配置的作用。

对于究竟使用注解来进行配置还是使用 XML 来进行配置,目前有很多争论,本文不讨论这些东西,我个人的观点是,对于经常改变的配置,建议使用 XML 文件,因为修改 XML 文件只需要重启应用程序就能生效,而修改注解就意味着修改 Java 源代码,所以要重新编译才能生效。而对于不经常改变的配置,建议使用注解,因为这样更加直观,与代码的联系也更加紧密,可读性也强。

解析 XML 文件很简单,就是将 XML 文档载入内存,建立 DOM 文档对象模型,Java 中有非常多的 API 用来解析 XML 文件。不多讨论。我们主要关注如何解析注解的元数据。对于编译期间的注解,解析的工具有 APT(Annotation Processing Tool,注解处理器,jdk7 之后被移除,使用 JSR 269 API 替代),我也没接触过 APT,也不怎么讨论;而对于运行期间的注解,就很好办了,就是使用 JAVA 反射 API 来解析注解。

在详细分析注解的底层原理之前,我先把注解原理的概括性描述放出来:

注解本质是一个继承了 Annotation 的接口,其具体实现类是 Java 运行时生成的 JDK 动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值(注解属性的值)。而 memberValues 的来源是 Java 常量池。

我们先来写一个简单的运行时注解的解析例子:

MyAnnotation.java,注解类:

  1. package com.zfl9;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.TYPE)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface MyAnnotation {
  9. String value() default "@MyAnnotation Default Value";
  10. }

AnnotationDemo.java,测试类:

  1. package com.zfl9;
  2. @MyAnnotation("Hello, MyAnnotation")
  3. public class AnnotationDemo {
  4. public static void main(String[] args) {
  5. MyAnnotation myAnnotation = AnnotationDemo.class.getDeclaredAnnotation(MyAnnotation.class);
  6. System.out.println(myAnnotation);
  7. System.out.println(myAnnotation.value());
  8. }
  9. }

运行结果如下:

  1. @com.zfl9.MyAnnotation(value=Hello, MyAnnotation)
  2. Hello, MyAnnotation

稍微解析一下:

我们定义了一个 @MyAnnotation 注解,Target 为 TYPE,意味着它可以用来标注“类型”,如类、接口、枚举。它的生命周期是 RUNTIME,所以我们可以在运行时通过反射来处理这个注解的元数据。

AnnotationDemo 类上我们用 MyAnnotation 标注了一下,value 为 "Hello, MyAnnotaion",然后在 main 方法中,通过 AnnotationDemo 的 Class 对象,获取标注在上面的 MyAnnotation 注解的实例,然后赋值给 MyAnnotation 接口(向上转型)。这个 MyAnnotation 实例是由 JDK 动态代理生成的代理对象。

JDK 动态代理我们知道,有两个重要的概念,一个是 java.lang.reflect.Proxy 类,一个是 java.lang.reflect.InvocationHandler 接口。Proxy 类是所有 JDK 动态代理机制生成的实例的共同父类,而 InvocationHandler 是调用处理器,委托类上的所有接口方法都将被调用处理器上实现,所有方法都会被传到调用处理器的 invoke() 方法中统一处理。

而 MyAnnotation 的实现类(JDK 动态代理类)只需实现 MyAnnotation 接口上定义的方法(在这里就是 String value() 方法咯),顶层接口 Annotation 上的方法不需要代理类实现,JDK 有默认实现逻辑。

而当我们调用 myAnnotation 实例的 value() 方法时,其实就是获取 myAnnotation 注解的属性值,记住了,注解上的元素的值,是通过调用其接口上的对应方法来获取的,这个也很明显,因为我们前面在定义注解时就看出来了,所谓注解元素就是接口上的方法。

好了,现在一切都很清楚了,注解就是接口,毫无疑问。现在唯一的疑问就是 JDK 动态代理类的 value() 方法是如何获取到注解元素的值的呢?开头的总结说了,注解上的元素值都会被存储在注解的 class 文件的常量池中,记住了,常量池(是的,正是因为注解上的元素类型只能是字面量、常量,所以我们能够将注解上的数据存放在常量池中),而使用反射 API 获取注解接口的实例时,会读取常量池中的注解元素,然后将它们放入一个名为 memberValues 的 Map 中,key 就是元素名(String),value 就是 Object(各种值),而 value() 方法就是从这个 map 中获取 key 为 value 的值,然后返回而已,好了,解析结束。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注