Android Annotation-让你的代码和设计更加优雅(一)

2,878 阅读10分钟

引子

最近我写了一篇关于组件化的开源框架源码分析的文章(传送门在下面儿)。那么现在组件化小有名气的JIMU框架,也是我下一个要给大家分享的源码分析文章。但因为其中涉及到了很多Java Annotation相关的知识。所以不得不在这里,先安利一下本篇,这也是本篇的由来。

优秀框架源码分析系列(一)让解耦更轻松!多进程组件化框架-ModularizationArchitecture

注解”,在Java世界里随处可见,但通常情况下,多数人对其是视而不见的。但当我们设计SDK,设计基础库的时候,运用注解,可以起到简化配置的作用。熟悉ButterKnife的朋友都知道,它就是通过注解来在编译期间,增加Java代码来实现的。如果你还不知道它是如何实现的,那么相信你食用完本篇和下一篇以后,就会明白这一切了。

食用路线

学习新知识的时候,要掌握正确的进食方法,脑子里必须先对知识结构有预期,学习完之后再回顾结构,根据结构记住知识。本篇将按照导图的结构,来进行讲解。

扎实打基础

基础知识-四大元注解

元注解,就是用来修饰注解的注解。

@Target(value=ElementType)

@Target被用来指明此Annotation所修饰的对象范围(即:被描述的注解可以用在什么地方)。Java中,注解(Annotation)可被用于以下位置 :

  • package、types(类、接口、枚举、Annotation类型)
  • 类型成员(方法、构造方法、成员变量、枚举值)
  • 方法参数和本地变量(如循环变量、catch参数)

注解的使用范围,通过Target的取值来指定。指定好以后,@Target修饰的元素一定是与其取的value相匹配的,否则编译会报错。

value取值(ElementType)常见的有:

ElementType 含义
CONSTRUCTOR 用于描述构造器
FIELD 用于描述域(包括enum常量)
LOCAL_VARIABLE 用于描述局部变量
METHOD 用于描述方法
PACKAGE 用于描述包
PARAMETER 用于描述参数
TYPE 用于描述类、接口(包括注解类型) 或enum声明

注:PACKAGE,它并不是使用在一般的类中,而是用在固定的文件package-info.java中。这里需要强调命名一定是“package-info”

这里需要特殊说明的是,在以前的Java版本中,开发者只能将注解(Annotation)写在声明中。但从Java 8开始,注解可以写在使用类型的任何地方,例如声明、泛型和强制类型转换等语句:

@Encrypted String str;
List<@NonNull String> strs;
test = (@Immutable Test) tmpTest;

针对这个拓展,JAVA8对原有的@Target的取值做了扩充,引入了新的类型(TYPE)注解,即ElmentType增加了:

ElementType 含义
TYPE_PARAMETER 表示注解可以用在类型变量的声明语句中(如 class Test {...})
TYPE_USE 表示注解可以用在使用类型的任何语句中(如声明语句、泛型和强转)

关于类型的解释参考上文。

@Retention(value=RetentionPolicy)

@Retention,翻译为保留,指示了一个注解被保留的时间期限,一个被其修饰的注解会被保留到其value指定三个阶段的其中之一,如果注解类型声明中不存在Retention注解,则保留策略默认为CLASS

RetentionPolicy 含义
RetentionPolicy.SOURCE 只在源代码级别保留,编译时就会被忽略
RetentionPolicy.CLASS 在编译时被保留,在class文件中存在,但JVM将会忽略
RetentionPolicy.RUNTIME 被JVM保留,所以他能在运行时被JVM或其他使用反射机制的代码所读取和使用

@Documented

被@Documented修饰的注解,用来表示这个被修饰注解应该被 javadoc工具记录。默认情况下,javadoc是不包括注解的。但如果声明注解时指定了@Documented,则它会被javadoc之类的工具处理,所以注解类型信息也会被包括在生成的文档中。

@Inherited

如果一个用来修饰class的注解,希望被这个class的sub-class继承,则可以对这个注解使用@Inherited修饰。 上面这句话强调了以下两点:

  • 注解的可继承性。当自定义的注解希望被继承时,就要使用@Inherited修饰
  • @Inherited只在修饰class时有效,修饰其他类型时无效

自定义注解如何写-Java Annotation的语法

自定义注解,通过@符号和interface关键字来定义。类似于class的写法,例如:

package com.xm.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnno{
    String name();
    String website() default "example.com";
    int version() default 1;
}

当注解需要参数的时候,我们需要先定义注解的方法,方法名即参数名。这里有点类似于接口的定义,如果参数需要默认值,则在方法后加default + 默认值的方式来实现。

例如本例中,我们指定了三个参数,name、website、version。分别定义了三个方法,name没有指定默认值,所以它默认为null,website指定了默认值为example.com,而version则指定了1为其默认值。

这里有三点规则强调一下:

  1. 注解方法不带参数,比如name()website()
  2. 注解方法返回值类型:基本类型、String、Enums、Annotation以及前面这些类型的数组类型
  3. 注解方法可有默认值,比如default "example.com",默认website=”example.com

当我们在使用时,需要给参数传递值,很简单:

@TestAnno(name="xiaoming", website="example.com", version=1)

后文对这个例子中的其他部分,还会有详细的解释。

高效学精髓

为了方便学习,我们拿最常见的Java内建的注解@Override来食用。把握一下自定义注解实现时的几个步骤。

1. 编写定义注解的Java文件

要自定义注解,首先要创建一个以注解名字命名的Java文件,并使用@interface关键字来定义注解

Override.java

package java.lang

public @interface Override {
}

2. 确定自定义注解的作用范围

这个也很容易理解,Override注解用来修饰方法,所以作用范围就指定为METHOD。

3. 确定自定义注解的作用时限

SOURCE、CLASS、RUNTIME的保留时限依次增加。而对于Override来说,我们日常编写代码时,通过这个注解可以知道哪些方法是被重写的。这个提示,也仅仅停留在了源码层面,所以这里使用SOURCE。

4. 确定自定义注解的参数、方法

Override并没有使用任何的参数和方法,这里也忽略了,后面我们实战例子里会重点介绍。

基于上述分析,我们写出了如下的定义源码:

package java.lang;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

通过以上的三个步骤,我们就定义好了一个注解。当我们使用时,直接通过@符号加注解名称即可。那么,加了注解的元素,有什么用呢?

其一,我们可以通过编译期处理,为注解所在的元素自动生成其他代码。像ButterKnife等均是如此,它帮助我们生成了View与组件的绑定,实现了点击监听器的绑定等等。保留到此时限的注解,我们也称其为编译期注解。

其二,我们在运行时,可以通过反射,获取到注解信息,这些注解信息,往往是程序编写者希望传递给运行时使用的一些信息或配置,可以起到简化配置的作用。像Spring2.5以后,运用了大量的运行时注解,它在实现AOP方面,有着广泛的应用。需要强调的是,这里的注解,都是RUNTIME规则的,只有这样才能保留到运行时。所以我们称其为运行时注解。

本篇我们重点介绍代码编写阶段的提示性注解和运行时注解。下面我们再通过两个实际例子来理解一下。

大胆练实战(一)

提示性注解示例

我们在java中,除了Override,还会经常见到一些其他的内建注解。例如SuppressWarnings(压制警告),他用于告知编译器忽略特定的警告信息,如在泛型中使用原始数据类型。

package java.lang;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target( { ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,
        ElementType.PARAMETER, ElementType.CONSTRUCTOR,
        ElementType.LOCAL_VARIABLE })
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    /**
     * The list of warnings a compiler should not issue.
     */
    public String[] value();
}

按我们上节所需编写自定义注解的几个步骤,来分别分析一下:

注解的作用范围

SuppressWarnings可用于除注解类型声明和包名之外的所有元素。所以这里的Target做了相应的指定。

注解的作用时限

这里的警告,都是静态语法检查类型的警告信息,所以这个注解也只需要保留在源码层面。即SOURCE

注解的参数方法

SuppressWarning指定了一个String类型的数组。它支持了多个字符串参数。其可取值为需要压制的警告类型,见下表:

参数 含义
deprecation 使用了过时的类或方法时的警告
unchecked 执行了未检查的转换时的警告
fallthrough 当Switch程序块进入进入下一个case而没有Break时的警告
path 在类路径、源文件路径等有不存在路径时的警告
serial 当可序列化的类缺少serialVersionUID定义时的警告
finally 任意finally子句不能正常完成时的警告
all 以上所有情况的警告

使用时,可按如下的方法赋值:

@SupressWarning(value={"uncheck","deprecation"})

运行时注解示例

针对我们上一节中自定义的注解TestAnno,我们来实践一下运行时注解。

准备工作

package com.xm.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnno{
    String name();
    String website() default "example.com";
    int version() default 1;
}

假设我们在如下的代码块中应用了此注解:

package com.xm.annotation;

public class AnnotationDemo {
    @AuthorAnno(name="xiaoming", website="example.com", version=1)
    public static void main(String[] args) {
        System.out.println("I am main method");
    }

    @SuppressWarnings({ "unchecked", "deprecation" })
    @AuthorAnno(name="suby", website="example2.com", version=2)
    public void demo(){
        System.out.println("I am demo method");
    }
}

针对注解解析

现在,我们在运行时,通过反射来解析自定义的注解@TestAnno,关于反射类位于包java.lang.reflect,其中有一个接口AnnotatedElement,该接口定义了注释相关的几个核心方法,如下:

返回值 方法 含义
T getAnnotation(Class annotationClass) 当存在该元素的指定类型注解,则返回相应注释,否则返回null
Annotation[] getAnnotations() 返回此元素上存在的所有注解
Annotation[] getDeclaredAnnotations() 返回直接存在于此元素上的所有注解
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 当存在该元素的指定类型注解,则返回true,否则返回false

前面我们自定义的注解,适用对象为Method。类Method继承类AccessibleObject,而类AccessibleObject实现了AnnotatedElement接口,那么可以利用上面的反射方法,来实现解析@TestAnno的功能(AnnotationParser.java),内容如下:

package com.xm.annotation;
import java.lang.reflect.Method;

public class AnnotationParser {
    public static void main(String[] args) throws SecurityException, ClassNotFoundException {
        String clazz = "com.xm.annotation.AnnotationDemo";
        Method[]  demoMethod = AnnotationParser.class
                .getClassLoader().loadClass(clazz).getMethods();

        for (Method method : demoMethod) {
            if (method.isAnnotationPresent(TestAnno.class)) {
                 AuthorAnno authorInfo = method.getAnnotation(TestAnno.class);
                 System.out.println("method: "+ method);
                 System.out.println("name= "+ authorInfo.name() +
                         " , website= "+ authorInfo.website()
                        + " , version= "+authorInfo.version());
            }
        }
    }
}

程序的输出结果:

method: public void com.xm.annotation.AnnotationDemo.demo()
name= suby , website= example2.com , version= 2
method: public static void com.xm.annotation.AnnotationDemo.main(java.lang.String[])
name= xiaoming , website= example.com , version= 1

由此可见,我们可以通过在编写代码时,将一些运行时需要的信息,通过注解的方式传递给运行时的代码,以达到信息传递和配置的目的。在Spring中,也大量采用了运行时注解,为程序的配置,以及程序开发,特别是AOP编程,都提供了极大的便利。

下一篇重点介绍编译期注解,在众多的知名框架或工具型SDK中,都能看见它的身影,它可以以一种极度优雅简洁的方式,为我们提供开发上的便捷

小铭出品,必属精品

欢迎关注xNPE技术论坛,更多原创干货每日推送。