Java基础之注解

问题

关于注解首先请思考一下以下问题:

  • 注解是什么?
  • 注解的主要使用场景?
  • 注解可以分为哪些类型?
  • 注解的处理过程?

接下来我们将对这几个问题进行探讨

注解简介

Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

上述是官方给出的关于注解的定义,大致意思是注解是元数据(MetaData)的一种形式,它用于提供一些和程序元素有关的元数据,这些数据本身不属于程序,并且也不会直接影响程序的操作。 为了更直观的理解注解的概念,我们还需要进一步了解元数据这个概念,关于元数据 Wiki 上的描述如下:

Metadata is "data [information] that provides information about other data",For example, a digital image may include metadata that describes how large the picture is, the color depth, the image resolution, when the image was created, the shutter speed, and other data.

元数据是描述数据的数据,对于一张相片而言元数据包括相片的大小、色彩深度、图片的分辨率、图片建立时间以及快门速度等相关数据。 结合两者可以得到:注解是一种用于描述程序元素信息的修饰符,可以用来修饰包、类、构造器、方法、成员变量、参数、局部变量。 这里再结合具体的场景进行理解:

1
2
@BindView(R.id.toolbar)
public Toolbar mToolbar;

对于成员变量 mToolbar 而言,其基本的信息包含 width、heigth、id,通过注解我们能够很明确的描述它的 id 信息。

注解的主要使用场景

要明白注解的使用场景,首先需要了解的是注解的主要作用:

  • Information for the compiler— Annotations can be used by the compiler to detect errors or suppress warnings.

  • Compile-time and deployment-time processing— Software tools can process annotation information to generate code, XML files, and so forth.

  • Runtime processing— Some annotations are available to be examined at runtime.

接下来举几个常见的例子说明:

  1. 为编译器提供信息用于检测错误或者抑制警告

    1
    2
    3
    4
    5
    @SuppressWarnings("unchecked")
    public void addItems(@NonNull String item) {
    List list = new ArrayList();
    list.add(item);
    }
  2. 编译时和部署时通过对注解进行处理生成代码、XML 文件等

    比较常见的如butterknife,butterknife 能够通过注解自动生成 findViewById 的代码,有助于减轻样板代码的负担

    1
    2
    @BindView(R.id.toolbar)
    public Toolbar mToolbar;
  3. 运行时通过注解进行检查处理

    在运行时我们可以通过反射机制对注解提供的信息进行处理,然后实现需要的功能。

在明确了注解的主要作用之后,注解的使用场景就已经呼之欲出了,当我们需要为程序中的元素提供信息,并且这些信息需要得到处理的时候,就可以考虑使用注解。

注解的分类

基本注解

Java 提供了 5 个基本的 Annotation

注解名 作用
@Override 限定重写父类方法
@Deprecated 表示某个程序元素已经过时
@SuppressWarnings 抑制编译器的警告
@SafeVarargs 抑制堆污染警告
@FunctionalInterface 指定某个接口必须是函数式接口

接下来聊聊它们的使用

  1. Override

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class FatherClass {
    public void overridedMethod() {
    }
    }

    public class BaseAnnotationUse extends FatherClass{

    @Override
    public void overridedMethod() {
    super.overridedMethod();
    }
    }
    Override的主要作用是:
    1. 帮助检查是否正确的复写了父类中的已有方法(如果不小心拼写错误或者方法签名对不上被覆盖的方法,编辑器都会发出警告信息)

    2. 表示当前方法定义将覆盖超类的方法。

      如果没有正确的复写父类中的方法则会提示 Method does not override method from its superclass

  2. Deprecated 在 FatherClass 增加如下方法:

    1
    2
    3
    4
    @Deprecated
    public void deprecatedMethod() {

    }

    然后在 BaseAnnotationUse 中增加:

    1
    2
    3
    public void useDeprecatedMethod() {
    deprecatedMethod();
    }

    这个时候将会看到 deprecatedMethod() 显示红色,并且编辑器提示deprecatedMethod() 已经过时了

  1. SuppressWarning 对 BaseAnnotationUse 中的 useDeprecatedMethod 方法进行如下修改

    1
    2
    3
    4
    @SuppressWarnings("deprecation")
    public void useDeprecatedMethod() {
    deprecatedMethod();
    }

    通过增加 SuppressWarning 抑制了 Deprecated 的警告,deprecatedMethod() 的红色将会消失

  1. SafeVarargs

    1
    2
    3
    4
    @SafeVarargs
    public static void faultyMethod(List<String>... listStrArray) {
    List[] listArray = listStrArray;
    }

    当把一个不带泛型的对象赋给一个带泛型的变量的时候,往往将会导致”堆污染“,所以在 Java 7 中增加了 SafeVarargs 用于抑制堆污染的警告,SafeVarargs 只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。

  2. FunctionalInterface

    1
    2
    3
    4
    @FunctionalInterface
    public interface BaseInterface {
    int add(int x, int y);
    }

    FunctionalInterface 是 Java 8 专门为 Lambda 表达式新增的,通过 FunctionalInterface 可以限制接口中只能存在一个抽象方法,如果在 BaseInterface 接口中新增抽象方法,编译时将会提示 BaseInterface 不是函数式接口。

元注解

JDK 在 java.lang.annotation 中内置了 6 种元注解,除了 Native 之外都用于修饰其它的 Annotation 定义

注解名 作用
@Retention 指定被修饰的注解的保留时间
@Target 指定被修饰的注解可以修饰的程序元素
@Documented 指定被修饰的注解可以被 javadoc 提取成文档
@Inherited 指定被修饰的注解具有继承性,如果某个类使用了被 @Inherited 修饰的注解,那么其子类将自动被该注解修饰
@Repeatable 用于定义重复注解
@Native 表示定义常量值的字段可以从本地代码引用。

接下来主要介绍前五种,@Native 实在是不常用

  1. Retention

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Retention {
    /**
    * Returns the retention policy.
    * @return the retention policy
    */
    RetentionPolicy value();
    }

    Retention的定义如上,其拥有一个 RetentionPolicy 类型的成员变量,RetentionPolicy 是枚举类,主要有三个枚举值:

    • RetentionPolicy.SOURCE 注解只能保留在源文件当中,编译器不会编译这种注解
    • RetentionPolicy.CLASS 注解能够保留在 class 文件当中,但是当程序运行的时候,JVM 不能够获取到注解信息
    • RetentionPolicy.RUNTIME 注解能够保留在 class 文件当中,程序运行的时候,JVM 也能够获取到注解信息,程序能够通过反射去获取到 Annotation 信息
  2. Target

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Target {
    /**
    * Returns an array of the kinds of elements an annotation type
    * can be applied to.
    * @return an array of the kinds of elements an annotation type
    * can be applied to
    */
    ElementType[] value();
    }

    ElementType 也是一个枚举类,其枚举值和意义是:

    • ElementType.TYPE 表明注解可以修饰类、接口或者枚举定义
    • ElementType.FIELD 表明注解可以修饰成员变量
    • ElementType.METHOD 表明注解可以修饰方法定义
    • ElementType.PARAMETER 表明注解可以修饰参数
    • ElementType.CONSTRUCTOR 表明注解可以修饰构造函数
    • ElementType.LOCAL_VARIABLE 表明注解可以修饰局部变量
    • ElementType.ANNOTATION_TYPE 表明注解可以修饰注解
    • ElementType.PACKAGE 表明注解可以修饰包定义
    • ElementType.TYPE_PARAMETER 表明注解只能定义程序元素的修饰
    • ElementType.TYPE_USE 表明注解不仅可以在定义程序元素的时候使用,还可以在创建对象、类型转换、使用 implements 实现接口、使用 throws 声明抛出异常的时候使用
  3. Documented 对于使用被 @Documented 修饰的注解和不带 @Documented 修饰的注解,其区别如下:

annotation_1
annotation_1
annotation_2
annotation_2
  1. Inherited

    Inherited 的继承作用可以通过以下例子来体现,首先创建一个 Interitable 注解

    1
    2
    3
    4
    5
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.TYPE)
    @Inherited
    public @interface Inheritable {
    }

    然后创建一个由 Inheritable 修饰的 Base 类

    1
    2
    3
    @Inheritable
    public class Base {
    }

    最后创建一个继承自 Base 类的 InheritableTest 类

    1
    2
    3
    4
    5
    6
    7
    public class InheritableTest extends Base {

    public static void main(String[] args) {
    //判断是否被 Inheritable 注解
    System.out.println(InheritableTest.class.isAnnotationPresent(Inheritable.class));
    }
    }

    运行之后可以发现结果为 true

  2. Repeatable

    Repeatable 是 Java8 新增的注解,用于定义重复注解,在 Java8 之前的重复注解只能写成以下形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Infos({@Info(name = "zhangsan"), @Info(name = "lisi")})
    private Person mPerson;
    public class Person {

    private String mName;
    private int mAge;
    private int mHeight;
    private int mWeight;
    }

    其中 Info 和 Infos 分别是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS)
    public @interface Info {

    String name();
    }

    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface Infos {

    Info[] value();
    }

    由于 Infos 中保留了 Info 的信息,所以 Infos 的保留时间不能比 Info 少,否则编译器将会报错。如果 Info 的保留时间是 RUNTIME,而 Infos 的保留时间是 SOURCE,那么 JVM 最终会丢弃 Infos 以及 Infos 中的 Info 信息,这与 Info 期望的保留时间相矛盾。

自定义注解

JDK 内置的注解并不能完全满足我们的需求,大多数情况下我们还需要学会自定义注解,定义一个注解需要用到 @Interface 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {

/**
* @return View 的 Id
*/
int value();
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindOnClickListener {

/**
* @return 实现 OnClickListener 接口的类
*/
Class<? extends OnClickListener> value();
}

上面定义了两个注解用于绑定 View 的 id 以及 点击事件,并且指定了注解可以修饰的元素以及保留时间,可以看到定义一个注解与定义一个接口非常类似,而实际上每个注解都是继承自 Annotation 接口的接口,反编译 BindView 生成的 class 文件可以看到

1
2
3
4
Compiled from "BindView.java"
public interface com.rookieyang.runtimeannotation.customizeannotation.BindView extends java.lang.annotation.Annotation {
public abstract int value();
}

这里自定义的注解都是含有成员变量的,而注解除了按照基本注解、元注解、自定义注解进行分类之外,我还可以根据是否包含成员变量将它分为两类:

  • 标记注解:这种注解没有成员变量,它仅仅通过是否存在来提供信息,如@Override、@Deprecated

  • 元数据注解:这种注解包含成员变量,它通过成员变量提供更多的信息,如@Retention、@Target

注解的处理

在使用自定义的注解的时候,如果不提供注解的处理工具,注解是不会自动生效的,注解的处理方法主要有两种,一种是运行时处理注解,一种是编译时处理注解。

  1. 运行时处理注解

    运行时处理注解主要利用 Java 的反射机制,接下来将结合具体实例说明如何通过反射处理注解。

    首先利用自定义注解部分定义的两个注解 BindView 和 BindOnClickListener 对 View 进行注解

    1
    2
    3
    @BindView(R.id.hello_world)
    @BindOnClickListener(CustomizeOnClickListener.class)
    private Button mHelloWorld;

    然后在 Activity 中定义一个 CustomizeOnClickListener 内部类

    1
    2
    3
    4
    5
    6
    7
    8
    class CustomizeOnClickListener implements OnClickListener {

    @Override
    public void onClick(View view) {
    Toast.makeText(MainActivity.this, R.string.hello_world,
    Toast.LENGTH_SHORT).show();
    }
    }

    接着定义一个处理注解的类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class AnnotationProcess {

    public static void process(Object obj) {
    Field[] fields = obj.getClass().getDeclaredFields();
    Activity activity;

    if (!(obj instanceof Activity)) {
    throw new RuntimeException("传入的参数不是Activity");
    }

    activity = (Activity) obj;

    for (Field field : fields) {
    try {
    View view = null;
    field.setAccessible(true );
    BindView bindView = field.getAnnotation(BindView.class);
    BindOnClickListener bindOnClickListener = field
    .getAnnotation(BindOnClickListener.class);
    //判断是否是 View
    boolean isView = View.class.isAssignableFrom(field.getType());
    if (bindView != null && isView) {
    view = activity.findViewById(bindView.value());
    //设置 obj 对象当中的 field 值为 view
    field.set(obj, view);
    }
    if (bindOnClickListener != null && isView && view != null) {
    Class<? extends OnClickListener> listener = bindOnClickListener.value();
    //实例化 CustomizeOnClickListener 内部类
    OnClickListener onClickListener = listener.getConstructor(
    obj.getClass()).newInstance(activity);
    view.setOnClickListener(onClickListener);
    }

    } catch (Exception e) {
    Log.i(obj.getClass().getSimpleName(), e.getMessage());
    }
    }
    }
    }

    最后在 Activity 中调用即可:

    1
    AnnotationProcess.process(this);
  2. 编译时处理注解

    相对与运行时利用反射处理注解会有性能损失而言,编译时处理注解利用 APT(Annotation Processing Tool)对注解进行处理然后生成代码、XML 文件,利用 APT 去处理注解性能更好。参照 JDK 文档中对于 Processor 接口的描述,我们可以大致知道注解处理器的工作流程

    1. If an existing Processor object is not being used, to create an instance of a processor the tool calls the no-arg constructor of the processor class.
    2. Next, the tool calls the init method with an appropriate ProcessingEnvironment.
    3. Afterwards, the tool calls getSupportedAnnotationTypes, getSupportedOptions, and getSupportedSourceVersion. These methods are only called once per run, not on each round.
    4. As appropriate, the tool calls the process method on the Processor object; a new Processor object is not created for each round.

    编译工具将会通过注解处理器的无参构造函数实例化一个注解处理器对象,然后调用注解处理器的 init 方法并传入 ProcessingEnvironment,之后则调用 getSupportedAnnotationTypes,getSupportedOptions和getSupportedSourceVersion,最后将会调用 process 方法。 接下来将说明如何利用 APT 生成一个类文件用于显示 HelloWorld:

    首先需要明确希望生成的类文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.rookieyang.myannotationtwo;

    import android.content.Context;
    import android.widget.Toast;

    public class MainActivity_HelloWorld {

    public static void show(Context context) {
    Toast.makeText(context, "HelloWorld", Toast.LENGTH_SHORT).show();
    }

    }

    想要生成这个类文件需要获取到两点信息:包名和类名,通过包名可以使类文件生成在使用注解的包下,便于解析注解的时候加载类文件,通过类名加上 "_HelloWorld" 确保生成的类文件唯一存在,同时也达到了使用注解的类和生成的类绑定的效果。

    接下来需要创建两个 Module,其中一个定义了所有的注解,另外一个定义了 APT,之所以需要定义两个 Module 的原因,其一为了让工程结构更清晰,另一方面定义 APT 需要用到 javax 包。整体的工程结构如下图所示:

    之后需要为 annotations-compiler 和 app 模块引入相关依赖

    1
    2
    3
    4
    5
    6
    7
    annotations-compiler 模块
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    compile project(path: ':annotations')

    app 模块
    compile project(path: ':annotations')
    annotationProcessor project(':annotations-compiler')

    其中 auto-service 的作用是帮助我们生成下列文件,主要作用是声明注解处理器。

    annotationProcessor 则是为模块指定注解处理器

    配置之后首先在 annotations 模块定义一个 HelloWorld 注解

    1
    2
    3
    4
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS)
    public @interface HelloWorld {
    }

    然后在 annotations-compiler 模块编写对应注解处理器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    //标识一个注解处理器
    @AutoService(Processor.class)
    public class AnnotationsCompiler extends AbstractProcessor {

    private Elements mElements;
    private Filer mFiler;
    private Messager mMessager;

    /*
    * 用于初始化 mElements、mFiler、mMessager
    */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvi) {
    super.init(processingEnvi);

    mElements = processingEnvi.getElementUtils();
    mFiler = processingEnvi.getFiler();
    mMessager = processingEnvi.getMessager();
    }

    /**
    *
    * @param elements 实际上传入的都是 {@link #getSupportedAnnotationTypes()}
    * 中支持的并且被扫描到(使用过)注解元素,例如这里获取到的类元素就是 HelloWorld 注解
    * @param roundEnvi 一个注解处理工具框架,通过它可以查询到使用了注解的元素
    * @return 返回处理结果
    */
    @Override
    public boolean process(Set<? extends TypeElement> elements,
    RoundEnvironment roundEnvi) {
    String packageName;
    String className;
    //获取使用了 HelloWorld 注解的元素
    for (Element element : roundEnvi.getElementsAnnotatedWith(HelloWorld.class)) {
    //通过 Elements 去获取包名
    packageName = mElements.getPackageOf(element).toString();
    //HelloWorld 注解只能被用在成员变量,所以通过获取外层元素就可以获取到使用注解的元素所在的类
    className = element.getEnclosingElement().getSimpleName() + "_HelloWorld";
    try {
    //通过 Filer 指定的路径下创建一个 java 源文件,然后写入对应的代码
    JavaFileObject javaFileObject = mFiler.createSourceFile(
    packageName + "." + className);
    Writer writer = javaFileObject.openWriter();
    writer.write("package " + packageName + ";\n\n");
    writer.write("import android.content.Context;\n");
    writer.write("import android.widget.Toast;\n\n");
    writer.write("public class " + className + " {\n\n");
    writer.write("\tpublic static void show(Context context) {\n");
    writer.write("\t\tToast.makeText(context,"
    + " \"HelloWorld\", Toast.LENGTH_SHORT).show();\n");
    writer.write("\t}\n");
    writer.write("\n}");
    writer.flush();
    writer.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    return false;
    }

    /**
    *
    * @return 返回注解处理器支持的 Java 版本
    */
    @Override
    public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
    }

    /**
    *
    * @return 返回注解处理器支持的注解集合
    */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotationTypes = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
    annotationTypes.add(annotation.getCanonicalName());
    }
    return annotationTypes;
    }

    /**
    *
    * @return 返回支持的注解类型集合
    */
    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotationSet = new LinkedHashSet<>();
    annotationSet.add(HelloWorld.class);

    return annotationSet;
    }
    }

    接下来说下几个有助于理解注解处理器的点:

    • Elements 是一个获取程序元素信息的接口,例如获取元素的包名、判断是否是重写方法,例如

    • Filer 则是一个支持通过注解处理器创建文件的接口,可以用于创建 Class文件、源文件、资源文件

    • Messager 则是为注解处理器提供的输出错误信息的接口。

    • Element 与 Elements 区别在于 Element 是获取单个程序元素信息的接口,而 Elements 可以获取整个程序的元素信息。除了 Element 之外,上述程序还可以看到 TypeElement,而 TypeElement 是一个继承了 Element 接口的接口,用于表示类元素。实际上 JDK 还提供了很多继承自 Element 的接口用于表示程序中的各项元素,具体的 Element 继承结构如下图所示:

    在编写完注解处理器之后,最后在 App 模块中编写对应的调用代码即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private void show() {
    try {
    //获得生成的类的 Class 对象
    Class<?> helloClass = Class.forName(getPackageName() + "."
    + getClass().getSimpleName() + "_HelloWorld");
    //利用反射取得 show 方法,然后执行对应的方法即可
    Method showMethod = helloClass.getMethod("show", Context.class);
    showMethod.invoke(this, this);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    实际上编译时注解的整个处理过程是利用注解处理器对程序中使用了注解的元素进行遍历,从元素中提取所需的信息,然后生成类文件,最后在程序中加载生成的类并调用其中的方法。

    最后给出本次测试的工程链接 Annotation 测试

总结

最后让我们回答开始的几个问题

  1. 注解是一种用于描述程序元素信息的修饰符,可以用来修饰包、类、构造器、方法、成员变量、参数、局部变量。

  2. 当我们需要为程序中的元素提供信息,并且这些信息得到处理的时候,就可以考虑使用注解。

  3. 按照系统内置的注解,可以分为基本注解、元注解、自定义注解。按照是否有成员变量可以分为标记注解、元数据注解。按照处理方式,可以分为运行时注解、编译时注解

  4. 注解的处理过程主要为运行时通过反射处理和编译时通过注解处理器进行处理。

Thanks

  • 《Java 编程思想》
  • 《疯狂 Java 讲义》
  • Annotation Tutorials
  • 自己动手实现Java注解(Java Annotation in Action)
  • 注解处理器(Annotation Processor)原理简析
  • Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架
  • Android注解使用之注解编译android-apt如何切换到annotationProcessor