Retrofit解析4之注解

整体Retrofit内容如下:

由于Retrofit里面大量的用到了注解,为了让大家更好的学习Retrofit,特意准备了一篇Java注解,如果大家已经对Java注解已经很熟悉了,就略过,看下一篇文章
本篇文章主要讲解

  • 1、Java 注解技术基本概念
  • 2、Java 元注解
  • 3、标准注解/内建注解
  • 4、自定义注解
  • 5、注解处理器
  • 6、注解思维导图
  • 7、注解原理

一、Java注解技术基本概念

(一) 什么是注解

Annotation是java 5开始引入的新特征。中文名称一般叫注解。它提供了一种安全的类似于注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。

上面的类似官方的解释,那我们再来通俗的解释一下:
我们都知道在Java代码中使用注解是为了提升代码的可读性,也就是说,注释是给人看的(对编译器来说是没有意义上的)。注解可以看做注释的"加强升级版",它可以向编译器、虚拟机等解释说明一些事情(也就是说它对编译器等工具也是"可读"的)。比如我们非常熟悉的@Overrider 注解,它的作用是告诉编译器它所注解的方法是重写父类中的方法,这样编译器就会检查父类是否存在这个方法,以及这个方法的签名与父类是否相同。
也就是说,注解是描述Java代码的代码,它能够被编译器解析,注解处理工具在运行时也能够解释注解。除了向编译器等传递一些信息,我们也可以用注解生成代码。比如我们可以用注解描述我们的意图,然后让注解解析工具来解析注解,以此来生成一些"模板化"的代码。注解是一种"被动"的信息,必须有编译器或虚拟机来"主动"解析它,它才能发挥自己的作用。

(二) 什么是元数据(metadata)

元数据由metadata翻译来的,所谓元数据就是"关于数据的数据",更通俗的说就是描述数据的数据的,对数据及信息资源的描述性信息,比如一个文本文件,有创建时间、创建人、文件大小等数据,都是可以理解为是元数据。在java中,元数据以标签的形式存在java代码中,它的存在并不影响程序代码的编译和执行,通常它被用来生成其他的文件或运行时知道被运行代码的描述信息。java代码中的javadoc和注解都属于元数据。

(三) 注解的前世今生

注解首先在第三版的Java Language Specification中被提出,并在Java 5中被实现。

(四)为什么要使用注解

  • 1、在未使用Annotation之前(甚至是使用之后),一般使用XML来应用于元数据的描述。不是何时开始一些开发人员和架构师发现XML的维护原来越复杂和糟糕,他们希望使用一些和代码紧密耦合的东西,而不是像XML一样是松耦合的(在某些情况下甚至是完全分离的)代码描述。如果你在百度或者google中搜索"xml vs annotations",就会看到关于这个话题的辩论。因为XML的配置就是为了分离代码和配置而设置的。但是用Annotation(注解)还是XML各有利弊。(下面有举例说明)
  • 2、另外一个很重要的因素是Annotation注解定义一种标准的描述元数据的方式。在这之前,开发者通常使用他们自己的方式定义元数据。例如,使用标记interface,注释,transient关键字等。每个程序员都用自己的方式定义元数据,而不像Annotation这种标准的方式。

下面我简单举例说明。
比如,你想为你的应用设置很多常量或参数,这种情况下,XML是一个很好的选择,因为它不会与特定的代码关联。如果你想把某个方法声明为服务,那么使用Annotation会更好一些,因为这种情况下需要注解和方法高度耦合一起。

因为XML是松耦合的,注解是紧耦合的,所以目前主流的框架将XML和Annotation两种方式结合使用,平衡两者之前的利弊。
在需要高度耦合的地方,Annotation注解比XML更容易维护,阅读更方便
在需要松耦合的地方,使用XML更方便
在某个方法声明为服务时,这种紧耦合的情况下,比较适合Annation注解。

(五)、注解的作用

Annotation 注解 通常被用以作以下目的:

  • 1、编译器指令
  • 2、构建时指令
  • 3、运行时指令
    Java 内置了三种编译器指令,Java注解可以应用于构建时,即当你构建你的项目时,构建的过程包括产生源代码、编译源代码、产生xml文件,将编译过的代码或者文件打包进jar文件等。通常情况下,注解不会出现在编译之后的Java代码中,但是想要出现也是可以的。Java支持运行时注解。这些注解可以通过java反射访问,运行时注解主要是提供给程序或者第三方API一些指令。

(六) 注解基础

一个简单的Java注解 类似于下面的这种 @Doctor ,"@" 符号告诉编译器这是一个注解,跟在"@" 符号后面的是注解的名字,上述的例子中注解的名字是Doctor。

(七) 注解元素

Java 注解可以使用元素设置一些值,元素类似于属性或者参数。下面是一个包含元素注解的例子

@Doctor (name = "张三")

上述注解的元素名称是name,值是"张三",没有元素的注解不需要括号。注解可以包含多个元素,下面就是包含多个元素的例子

@Doctor(name = "张三", sex= "男")

当注解只包含一个元素时,你可以省去写元素的名字,直接赋值即可。下面的例子就是直接赋值。

@InsertNew("yes")

(八)注解使用

Annotation 注解可以在以下场合被使用到

  • 接口
  • 方法
  • 方法参数
  • 属性
  • 局部变量

二、元注解

(一) 什么是元注解

元注解,元注解就是负责注解其它注解.Java5.0定义了4个标准的meta-annotation类型,它们呗用来提供对其他annotation类型作说明。Java5.0定义的元注解:

  • @Target
  • @Retention
  • @Documented
  • @Inherited

这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们来看一下每一个元注解的作用和说明

1、@Target

表示该注解可以用在什么地方,由ElementType枚举定义

  • CONSTRUCTOR:构造器的声明
  • FIELD:域声明(包括enum实例)
  • LOCAL_VARIABLE:布局变量声明
  • METHOD:方法声明
  • PACKAGE:包声明
  • PARAMETER:参数声明
  • TYPE:类、接口(包括注解类型)或enum声明
  • ANNOTATION_TYPE:注解声明(应用于另一个注解上)
  • TYPE_PARAMETER:类型参数声明(1.8新加入)
  • TYPE_USE:类型使用声明(1.8新加入)
    PS: 当注解未制定Target值时,此注解可以使用任何元素之上,就是上面的类型。

举例如下:

@Target(ElementType.METHOD)
public @interface MethodInfo { 
}

上面代码中我们使用"@Target"元注解来说明MethodInfo这个注解只能应用于对方法进行注解。

2、@Retention

表示需要在什么级别保存该注解信息,由RetentionPolicy枚举定义

  • SOURCE:注解将编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里)
  • CLASS:注解在class中可用,但会被VM丢弃(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中(JVM)中)
  • RUNTIME:VM将在运行期也保留注解信息,因此可以通过反射机制读取注解信息(源码、class文件和执行的时候都有注解的信息)

PS:当胡姐未定义Retention值时,默认值是CLASS

@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

这表明@Override 注解只在源码阶段存在,javac在编译过程中去掉该注解。

3、@Documented

表示注解会被包含在javaapi文档中
当一个注解被@Documented元注解所修饰时,那么无论在哪里使用这个注解,都会被Javadoc工具文档化。
举例如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)public @interface Documented {
}

这个元注解呗@Documented修饰,表示它本身会被文档化。@Retention注解的值RetentionPolicy.RUNTIME表示@Documented这个注解能保留在运行时;@Target元注解的值ElementType.ANNOTATION_TYPE表示@Documented这个注解只能够来修饰注解类型

4、@Inherited

允许子类继承父类的注解。
用于描述某个被标注的类型可被继承的,如果一个使用了@Inherited修饰的annotation类型类型被用于一个class,则这个annotation将被用于该class类的子类。
表明被修饰的注解类型是自动继承的。如果你想让一个类和它的子类都包含某个注解,就可以使用@Inherited来修饰这个注解。也就是说,假设@Parent类是Child类的父类,那么我们若用被@Inherited元注解所修饰的某个注解对Parent类进行了修饰,则相当于Child类也被该注解所修饰了。这个元注解的定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)public @interface Inherited {
}
@Inherited
public @interface MyAnnotation {
}

@MyAnnotation
public class MySuperClass {
}

public class MySubClass extends MySuperClass {
}

上述代码的大致意思是使用@Inherited修饰注解MyAnnotation使用MyAnnotation注解MySuperClass实现类MySubclass继承自MySuperClass

当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

三、标准注解/内建注解

Java本身内建了一些注解,用来为编译器提供指令。如下:

  • @Override
  • @Deprecated
  • @SuppressWarnings
    下面让我们详细了解下三个标准注解/内建注解

1、@Override注解

@Override注解用来修饰对父类进行重写的方法。如果一个并非重写父类的方法使用这个注解,编译器将提示错误。

实际上在子类中重写父类或接口的方法,@Overrider并不是必须的。但是还是建议使用这个注解,在某些情况下,假设你修改了父类的方法的名字,那么之前重写子类方法将不再属于重写,如果没有@Override,你将不会觉察到这个子类的方法。有了这个注解修饰,编译器则会提示你这些信息。
例子如下:

public class MySuperClass {

    public void doTheThing() {
        System.out.println("Do the thing");
    }
}


public class MySubClass extends MySuperClass{

    @Override
    public void doTheThing() {
        System.out.println("Do it differently");
    }
}

2、@ Deprecated

@Deprecate 标记类、方法、属性,如果上述三种元素不再使用,使用@Deprecated注解,建议用户不再使用

如果代码使用了@Deprecate 注解的类、方法或属性,编译器会进行警告。
举例如下:

@Deprecated
public class MyComponent {
}

当我们使用@Deprecate注解后,建议配合使用对应的@deprecated JavaDoc 符号,并解释说明为什么这个类,方法或属性被弃用,已经替代方案是什么?如下:

@Deprecated
/**
  @deprecated This class is full of bugs. Use MyNewComponent instead.
*/
public class MyComponent {
}

3、@SuppressWarnings

@SuppressWarnings 用来抑制编译器生成警告信息。可以修饰的元素为类,方法,方法参数,属性,局部变量。

使用场景:当我们一个方法调用了弃用的方法或者进行不安全的类型转换,编译器会生成警告。我们可以为这个方法增加@SuppressWarnings
注解,来抑制编译器生成警告。

PS:使用@SuppressWarnings注解,采用就近原则,比如一个方法出现警告,我们尽量使用@SuppressWarnings注解这个方法,而不是注解方法所在的类。虽然两个都能抑制编译器生成警告,但是范围越小越好,因为范围到了,不利于我们发现该类下其他方法的警告信息。
举例如下:

@SuppressWarnings
public void methodWithWarning() {
}

四、自定义注解

1、注解格式

了解完系统注解之后,我们就可以自己定义注解了,通过上面的@Override的实例,不难看出定义注解的格式如下:

public @Interface 注解名{定义体}

PS:定义体就是方法的集合,每个方法实则是生命了一个配置参数,方法的名称作为配置参数的名称,方法的返回值类型就是配置参数的类型,和普通的方法不一样,可以通过default关键字来声明配置参数的默认值。
注意:

  • 1、注解类型是通过"@interface"关键字定义的
  • 2、此处只能使用public或者默认的default两个权限修饰符
  • 3、配置参数的类型只能使用基本类型(byte,boolean,char,short,int,long,float,double和String,Enum,Class,annotation)
  • 4、对于只含有一个配置参数的注解,参数名建议设置中value,即方法名为value.
  • 5、配置参数一旦设置,其参数值必须有确定的值,要不在使用注解的时候指定,要不在定义注解的时候使用default为其设置默认值,对于非基本类型的参数值来说,其不能为null。

2、创建自己的注解

在Java中,我们可以创建自己的注解,注解和类,接口文件一样定义在自己的文件里面。如下:

@interface MyAnnotation {
   String   name();
   int      age();
   String   sex();
 
 
}

上述代码定义了一个叫做MyAnnotation的注解,它有4个元素。再次强调一下,@Interface 这个关键字 用来告诉java编译器这是一个注解。
应用举例

@MyAnnotation(
    name="张三",
    age=18,
    sex="男"
)
public class MyClass {
}

注意,我们需要为所有的注解元素设置值,一个都不能少。

3、自定义注解默认值

对于注解总的元素,我们可以为其设置默认值,使用方法如下:

@interface MyAnnotation {
    String   name();
    int      age();
   String   sex() default "男";
}

上述代码,我们设置了sex元素的默认值为"男"。当我们在使用时,可以不设置sex的值,即让value使用空字符串默认值。举例如下;

@MyAnnotation(
    name="Jakob",
    age=37,
)
public class MyClass {
}

五、注解的原理

1、注解处理器

如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用户了,使用注解的过程中,很重要的一部分就是创建与使用注解处理器。Java SE 扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。

2、注解处理器的分类

我们已经知道了如何自定义注解,当时想要注解发挥实际作用,需要我们为注解编写响应的注解处理器,根据注解的特性,注解处理器可以分为运行时注解处理器编译时注解处理器。运行时注解处理器需要借助反射机制实现,而编译时处理器则需要借助APT来实现。

无论是运行时注解处理器还是编译时注解处理器,主要工作都是读取注解及处理特定主机,从这个角度来看注解处理器还是非常容易理解的。

3、运行时注解处理器

熟悉Java反射机制的同学一定对java.lang.reflect包非常熟悉,该包中的所有API都支持读取运行时Annotation的能力,即属性为@Retention(RetentionPolicy.RUNTIME)的注解。

在Java.lang.reflect中中的AnnotatedElement接口是所有程序元素的(Class、Method)父接口,我们可以通过反射获取到某个类的AnnotatedElement对象,进而可以通过该对象提供的方法访问Annotation信息,常用的方法如下:

方法 含义
< T extends Annotation > T getAnnotation(Class <T> annotationClass) 表示返回该元素上存在的定制类型的注解
Annotation[] getAnnotations() 返回该元素上存在的所有注解
default <T extends Annotation> T[] getAnnotationsByType(Class <T> annotationClass) 返回该元素制定类型的注解
default <T extends Annotation> T getDeclaredAnnotation( Class <T> annotationClass) 返回直接存在与该元素上的所有注解
default <T extends Annotation>T[] getDeclaredAnntationsByType(Class <T> annotationClass) 返回直接存在该元素上某类型的注解
Annotation[] getDeclaredAnnotations() 返回该元素上的所有注解

举例说明
一个User实体类

public class User {
    private int id;
    private int age;
    private String name;

    @UserData(id=1,name="张三",age = 10)
    public User() {
    }
    public User(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }
  //...省略setter和getter方法
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

我们希望可以通过@UserData(id=1,name="张三",age = 10)这个注解,来为设置User实例的默认值。
自定义注解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface UserData {
    public int id() default 0;
    public String name() default "";
    public int age() default 0;
}

该注解类作用于构造方法,并在运行时存在,这样我们就可以在运行时通过反射获取注解进而为User实例设值,看看如何处理该注解
运行时注解处理器:

public class AnnotationProcessor {

    public static void init(Object object) {

        if (!(object instanceof User)) {
            throw new IllegalArgumentException("[" + object.getClass().getSimpleName() + "] isn't type of User");
        }

        Constructor[] constructors = object.getClass().getDeclaredConstructors();
        for (Constructor constructor : constructors) {
            if (constructor.isAnnotationPresent(UserMeta.class)) {
                UserMeta userFill = (UserMeta) constructor.getAnnotation(UserMeta.class);
                int age = userFill.age();
                int id = userFill.id();
                String name = userFill.name();
                ((User) object).setAge(age);
                ((User) object).setId(id);
                ((User) object).setName(name);
            }
        }
    }
}

测试代码

public class Main {

    public static void main(String[] args) {
        User user = new User();
        AnnotationProcessor.init(user);
        System.out.println(user.toString());
    }
}

运行测试代码,便得到我们想要的结果:

User{id=1, age=10, name=’dong’}

这里通过反射获取User类声明的构造方法,并检测是否使用了@UserData注解。然后从注解中获取参数值并将其复赋值给User对象。
正如上面所说,运行时注解处理器的编写本质上就是通过反射获取注解信息,随后进行其他操作。编译一个运行时注解处理器就是那么简答。运行时注解通常多用于参数配置模块。

4、编译时注解处理器

不同于运行时注解处理器,编写编译时注解处理器(Annotation Processor Tool)。
APT 用于编译时期扫描和处理注解信息,一个特定的注解处理器可以以Java源文件或编译后的class文件作为输入,然后输出另一些文件,而已是.java文件,也可以是.class文件,但通常我们输出的是.java文件。(注意:并不是对源文件进行修改),这些java文件会和其他源文件一起被javac编译。
你可能会很纳闷,注解处理器是到底在什么阶段介入的呢?好吧,其实是在javac开始编译之前,这就是通常我们为什么愿意输出.java文件的原因。

注解最早是在java 5引入的,主要包含APT和com.sum.mirror包中现相关mirror api,此时APT和javac是各自独立的,但是从Java 6开始,注解处理器正式标准化,APT工具也被直接集成在javac当中。

编译时注解处理器编译一个注解时,主要分2步

  • 1、 继承AbstractProcessor,实现自己的注解处理器
  • 2、注册处理器,并打包成jar

举例说明:
首先来看一下一个标准的注解处理器的格式:

public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

来简单的了解下其中5个方法的作用

方法 作用
init(ProcessingEnvironment processingEnv) 该方法有注解处理器自动调用,其中ProcessingEnvironment类提供了很多有用的工具类:Filter,Types,Elements,Messager等
getSupportedAnnotationTypes() 该方法返回字符串的集合表示该处理器用于处理那些注解
getSupportedSourceVersion() 该方法用来指定支持的Java版本,一般来说我们都是支持到最新版本,因此直接返回SourceVersion.latestSupported()即可
process(Set annotations, RoundEnvironment roundEnv) 该方法是注解处理器处理注解的主要地方,我们需要在这里写扫描和处理注解的代码,以及最终生成的java文件。其中需要深入的是RoundEnvironment类,该用于查找出程序元素上使用的注解

编写一个注解处理器首先要对ProcessingEnvironment和RoundEnvironment非常熟悉。接下来我们来了解下这两个类,先看下ProcessingEnvironment类:

public interface ProcessingEnvironment {

    Map<String,String> getOptions();

    //Messager用来报告错误,警告和其他提示信息
    Messager getMessager();

    //Filter用来创建新的源文件,class文件以及辅助文件
    Filer getFiler();

    //Elements中包含用于操作Element的工具方法
    Elements getElementUtils();

     //Types中包含用于操作TypeMirror的工具方法
    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}

重点来认识一下Element,Types和Filer。Element(元素)是什么呢?

Element

element表示一个静态的,语言级别的构件。而任何一个结构化文档都可以看作是由不同的element组成的结构体,比如XML,JSON等。这里我们用XML来示例:

<root>
  <child>
    <subchild>.....</subchild>
  </child>
</root>

这段xml中包含了三个元素:<root>、<child>、<subchild>到现在你已经明白元素是什么。对java源文件来说,他同样是一种结构化文档:

package com.demo;             //PackageElement
public class Main{                  //TypeElement
    private int x;                  //VariableElement
    private Main(){                 //ExecuteableElement
    }
    private void print(String msg){ //其中的参数部分String msg TypeElement
    }
}

对于java源文件来说,Element代表程序元素:包,类,方法都是一种程序元素。另外如果你对网页解析工具jsoup熟悉,你会觉得操作此处的element是非常容易,关于jsoup不在本文讲解之内。

接下来看看各种Element之间的关系图,以便有个大概的了解


element关系图.png
元素 含义
VariableElement 代表一个 字段, 枚举常量, 方法或者构造方法的参数, 局部变量及 异常参数等元素
PackageElement 代表包元素
TypeElement 代表类或接口元素
ExecutableElement 代码方法,构造函数,类或接口的初始化代码块等元素,也包括注解类型元

TypeMirror、TypeElement、DeclaredType 这三个类我也简单的介绍下:

  • TypeMirror:代表Java语言中类型.Types包括基本类型,声明类型,数组,类型变量和空类型。也代表通配类型参数,可执行文件的签名和返回类型等.
  • TypeElement 代表类或接口元素
  • DeclaredType 代表类型或接口类型

简单的来说,Element代表源代码,TypeElement代表的是源码中的类型元素,比如类,虽然我们可以从TypeElement中获取类名,TypeElement中不包含类本身的信息,比如它的父类,要想获取这信息需要借助TypeMirror,可以通过的Element中的asType()获取元素对应的TypeMirror。

Filer

Filter 用于注解处理器中创新文件。

然后看一下RoundEnvironment这个类,这个类比较简单

public interface RoundEnvironment {

    boolean processingOver();

     //上一轮注解处理器是否产生错误
    boolean errorRaised();

     //返回上一轮注解处理器生成的根元素
    Set<? extends Element> getRootElements();

   //返回包含指定注解类型的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(TypeElement a);

    //返回包含指定注解类型的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}

然后来看一下RoundEnvironment,这个类比较简单,一笔带过:
public interface RoundEnvironment { boolean processingOver(); //上一轮注解处理器是否产生错误 boolean errorRaised(); //返回上一轮注解处理器生成的根元素 Set<? extends Element> getRootElements(); //返回包含指定注解类型的元素的集合 Set<? extends Element> getElementsAnnotatedWith(TypeElement a); //返回包含指定注解类型的元素的集合 Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);}

Filer
Filer用于注解处理器中创建新文件。具体用法在下面示例会做演示.另外由于Filer用起来实在比较麻烦,后面我们会使用javapoet简化我们的操作.
好了,关于AbstractProcessor中一些重要的知识点我们已经看完了.假设你现在已经编写完一个注解处理器了,下面,要做什么呢? |

打包并注册.

自定义的处理器如何才能生效那?为了让Java编译器找到自定义的注解处理器我们需要对其进行注册和打包:自定义的处理器需要被达成一个jar,并且需要在jar包的META-INF/services路径下中创建一个固定的文件
javax.annotation.processing.processor,在javax.annotation.processing.Processor文件中需要填写自定义处理器的完整路径名,有几个处理器就要填写几个
从Java 6之后,我们只需要将打开的jar防止到项目的buildpath下即可,javac在运行的过程会自动检查javax.annotation.processing.Processor注册的注解处理器,并将其注册上。而Java 5需要单独使用APT工具。
最终我们需要获得一个包含注解处理器的代码的jar包

六、注解基础知识思维导图

最后借用下别人的Java注解的基础知识点导图


Java注解.png

推荐阅读更多精彩内容