注解简单分析

本文详细介绍了Java中的注解,包括定义、作用分类、元注解、内建注解(如@Deprecated、@Override和@SuppressWarnings)以及自定义注解。注解在编译时和运行时的解析方式以及如何使用反射进行注解解析。此外,还探讨了编译时注解的处理,如注解处理器的工作原理和使用AutoService简化注解处理器的注册。

注解简单分析

一、定义

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

二、作用分类

  1. 编写文档:通过代码里标识的注解生成文档 (生成文档doc文档)

  2. 代码分析:通过代码里标识的注解对代码进行分析 (使用反射)

  3. 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查 (Override)

三、注解分类

3.1 元注解

元注解是由java API提供的,是专门用来定义注解的注解,常用的元注解主要包括Documented、Inherited、Retention、Target四个,其作用分别如下:

(1) Documented:

源码:

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

表示拥有该注解的元素可通过javadoc此类的工具进行文档化。注解只是一个标记的话,那么使用javadoc生成文档时,这些注解都不会存在于文档中,要使注解在文档中也存在,就可以在使用了注解的作用对象上加上@Documented这个注解。javadoc所生成的文档就会带上注解信息。

(2) Inherited:

源码:

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

该注解表示子类可以集成加载父类上的注解。但要注意:

  1. 注解定义在类上面,子类是可以继承该注解

  2. 注解定义在方法上面,子类也可以继承该注解,但是如果子类复写了父类中定义了注解的方法,那么子类将无法继承该方法的注解,也就是说,子类在复写父类中被@Inherited标注的方法时,会将该方法上面的注解覆盖掉

  3. Interface的实现类(implements实现)无法继承接口中所定义的被@Inherited标注的注解

(3) Retention:

源码:

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

表示该注解类型的注解保留的时长。当注解类型声明中没有@Retention元注解,则默认保留策略为RetentionPolicy.CLASS。关于保留策略(RetentionPolicy)是枚举类型,共定义3种保留策略,如下表:

RetentionPolicy含义
SOURCE仅存在Java源文件,经过编译器后便丢弃相应的注解
CLASS存在Java源文件,以及经编译器后生成的Class字节码文件,但在运行时VM不再保留注释
RUNTIME存在源文件、编译生成的Class字节码文件,以及保留在运行时VM中,可通过反射性地读取注解

(4) Target:

源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

表示该注解类型的所使用的程序元素类型。当注解类型声明中没有@Target元注解,则默认为可适用所有的程序元素。如果存在指定的@Target元注解,则编译器强制实施相应的使用限制。关于程序元素(ElementType)是枚举类型,共定义8种程序元素,如下表:

ElementType含义
ANNOTATION_TYPE注解类型声明
CONSTRUCTOR构造方法声明
FIELD字段声明(包括枚举常量)
LOCAL_VARIABLE局部变量声明
METHOD方法声明
PACKAGE包声明
PARAMETER参数声明
TYPE类、接口(包括注解类型)或枚举声明

3.2 内建注解

在内建注解中,常用到三种注解类,即常用到的Deprecated、Override和SuppressWarnings.

(1) Deprecated:

源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

用途:用于告知编译器,某一程序元素(比如方法,成员变量)不建议使用时,应该使用这个注解。Java在javadoc中推荐使用该注解,一般应该提供为什么该方法不推荐使用以及相应替代方法。

注解类型分析: @Deprecated可适合用于除注解类型声明之外的所有元素,保留时长为运行时VM。

在使用Eclipse或AS编写程序过程中,有一些方法编写出来之后被画上了中划线,表示该方法已过时,建议使用新的一些方法代替。如:viewPager.setOnPageChangeListener()方法在安卓API23中已过时,在上面被画上了中划线,在源代码中的该方法上赫然多了一个注解@Deprecated,可使用addOnPageChangeListener()方法代替。如果使用javac命令编译,会弹出:”注意:使用或覆盖了已过时的API“。

(2) Override:

源码:

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

用途:用于告知编译器,我们需要覆写父类的当前方法。这是开发过程中最常遇到的注解了,表示复写父类中的方法。如果方法上有这条注解但没有重写父类方法,则会生成一条错误消息。这个注解有效地保证了方法一定会被复写。比如开发过程中,手动在子类中复写父类方法,只正确写出了方法名称,未写正确方法的参数,则相当于方法的重载,并不是复写。这种问题如果在功能上有错误产生,在检查过程中是很难找到的。使用@Override注解,有效地表明,此方法是复写父类的方法,不会产生手动的错误问题。

注解类型分析:@Override可适用元素为方法,仅仅保留在java源文件中。

(3) SuppressWarnings:

源码:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

用于:用于告知编译器忽略特定的警告信息,例在泛型中使用原生数据类型。

注解类型分析: @SuppressWarnings可适合用于除注解类型声明和包名之外的所有元素,仅仅保留在java源文件中。

该注解有方法value(),可支持多个字符串参数,例如:

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

前面讲的@Override,@Deprecated都是无需参数的,而压制警告是需要带有参数的,可用参数如下:

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

对比:

3种内建注解的对比:

内建注解TargetRetention
OverrideMETHODSOURCE
SuppressWarnings除ANNOTATION_TYPE和PACKAGE外的所有SOURCE
Deprecated除ANNOTATION_TYPE外的所有RUNTIME

3.3 自定义注解

从上面的注解中,也大致看到了注解的定义方式。

  1. 使用@interface定义一个注解类,其内部自行继承了Annotation类。

  2. 在该类中定义注解的参数,定义方式很像方法。

  3. 注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。定义一些特殊的值,例如空字符串或者负数,表示某个元素不存在参数设置默认值时,后面跟上default即可设置默认值。

  4. 当注解的参数只有一个时,建议用value作为参数名,这样在使用注解时,可以直接写参数的值。
    下面我们举一个例来看一下注解自定义和使用的一般形式。

接下来自定义一个注解类:

package com.example.testannotation;
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 PersonAnnotation{
    String name();
    String qq() default "594695686";
    String address();
}

有了前面的自定义注解@PersonAnnotation,那么我们便可以在代码中使用(Demo.java),如下:

package com.example.testannotation;

public class Demo {

    @PersonAnnotation(name="jack",address = "beijing")
    public void demo1(){
        System.out.println("I am demo1 method");
    }

    @PersonAnnotation(name="tom",qq = "2769703983",address = "shanghai")
    public void demo2(){
        System.out.println("I am demo2 method");
    }
}

四、注解解析

介绍注解解析之前需要了解一下编译时注解和运行时注解。

网上常说的注解,基本是运行时注解,而所说的注解会影响性能,则是指的此类型的注解。因为运行时注解的解析,完全依赖于反射,而反射的效率,是比原生的慢的,特别是对于原先的老机型,本来配置就低,运行就卡,再使用过多注解,运行时去反射解析,就导致运行效率更慢了。这也就是网上老司机们所说的注解会影响性能的主因了。

而对于编译时注解,就不一样了。我们都知道,我们写的java文件,会先经过编译,将java文件编译成.class文件,再对class文件进行打包等一系列处理,生成apk,最终才运行到我们的手机上。所以编译时注解,是在java编译生成.class文件这一步进行的操作,根本和我们的apk运行,没半毛钱关系,所以效率问题也就无从说起了。

区分注解使用的是运行时。还是编译时。我们只需要看注解的定义就行。如:

编译时注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Params {
    ...
}

运行时注解:

@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Field {
    ...
}
4.1 运行时注解解析

由于1.3举例中注解的保留策略为RetentionPolicy.RUNTIME,故可在运行期通过反射机制来使用,否则无法通过反射机制来获取。

通过反射技术来解析自定义注解@PersonAnnotation,关于反射类位于包java.lang.reflect,注解相关的几个核心方法,如下:

返回值方法解释
TgetAnnotation(Class annotationClass)当存在该元素的指定类型注解,则返回相应注释,否则返回null
Annotation[]getAnnotations()返回此元素上存在的所有注解
Annotation[]getDeclaredAnnotations()返回直接存在于此元素上的所有注解。
booleanisAnnotationPresent(Class annotationClass)当存在该元素的指定类型注解,则返回true,否则返回false

前面自定义的注解,适用对象为Method。利用上面的反射方法,来实现解析@PersonAnnotation的功能(MainActivity.java),内容如下:

package com.example.testannotation;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.lang.annotation.Documented;
import java.lang.reflect.Method;

public class MainActivity extends AppCompatActivity {

    private TextView show;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        show = (TextView)findViewById(R.id.show);
        String clazz = "com.example.testannotation.Demo";
        Method[]  demoMethods = new Method[0];
        try {

            //获取MainActivity中的所有方法
            demoMethods =MainActivity.class
                    .getClassLoader().loadClass(clazz).getMethods();
            String res="";

            //遍历方法
            for (Method method : demoMethods) {

                //如果PersonAnnotation注解存在于method上
                if (method.isAnnotationPresent(PersonAnnotation.class)) {

                    //获得PersonAnnotation注解对象
                    PersonAnnotation personAnnotation = method.getAnnotation(PersonAnnotation.class);

                    //获得注解元素
                    String str = "method: "+ method +"\n"
                            +"name= "+ personAnnotation.name() +
                            " , website= "+ personAnnotation.qq()
                            + " , revision= "+personAnnotation.address()+"\n\n";
                    res += str;
                }
            }
            show.setText(res);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行时注解解析的实现主要通过反射,比较容易理解在这里不做过多讲解,我们将重点放到编译时注解解析的分析。

点击此处获取例程源码

4.2 编译时注解

说到编译时注解(RetentionPolicy.CLASS)都要和注解处理器(Annotation Processor) 扯上关系,因为这里是真正体现编译时注解价值的地方。需要注意的一点是,运行时注解(RetentionPolicy.RUNTIME)和源码注解(RetentionPolicy.SOURCE)也可以在注解处理器进行处理,不同的注解有各自的生命周期,根据你实际使用来确定。

注解处理器(Annotation Processor)

首先来了解下什么是注解处理器,注解处理器是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件作为输出。

下面举例说明编译时注解解析。

自定义注解(RetentionPolicy.CLASS)

先来定义要使用的注解,这里建一个Java库来专门放注解,库名为:annotations,和下面要创建的注解处理器分开。自定义注解如下:

@Target({ ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.CLASS)
public @interface Person
{
}

注解库指定JDK版本为1.8,如何指定往下看。

定义的是编译时注解,对象为类或接口等。

定义注解处理器

下面来定义注解处理器,另外建一个Java库工程,库名为:processors,记得是和存放注解的库分开的。注意,这里必须为Java库,不然会找不到javax包下的相关资源。来看下现在的目录结构:

这里写图片描述

这里定义一个注解处理器 PersonProcessor,每一个处理器都是继承于AbstractProcessor,并要求必须复写 process() 方法,通常我们使用会去复写以下4个方法:

/** 
* 每一个注解处理器类都必须有一个空的构造函数,默认不写就行; 
*/ 

public class PersonProcessor extends AbstractProcessor {

    // 元素操作的辅助类
    Elements elementUtils;

    /*
    *init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
    *ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
    * @param processingEnvironment 提供给 processor 用来访问工具框架的环境
    */

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

        // 元素操作的辅助类
        elementUtils = processingEnv.getElementUtils();
    }

    /**
     * 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
     * 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
     * @param set   请求处理的注解类型
     * @param roundEnvironment  有关当前和以前的信息环境
     * @return  如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
     *           如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
     */

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        // 获得被该注解声明的元素
        Set<? extends Element> elememts = roundEnvironment
                .getElementsAnnotatedWith(Person.class);
        TypeElement classElement = null;// 声明类元素
        List<VariableElement> fields = null;// 声明一个存放成员变量的列表

        // 存放二者
        Map<String, List<VariableElement>> maps = new HashMap<String, List<VariableElement>>();

        // 遍历
        for (Element ele : elememts) {

            // 判断该元素是否为类
            if (ele.getKind() == ElementKind.CLASS) {
                classElement = (TypeElement) ele;
                maps.put(classElement.getQualifiedName().toString(),
                        fields = new ArrayList<VariableElement>());

            } else if (ele.getKind() == ElementKind.FIELD) // 判断该元素是否为成员变量
            {
                VariableElement varELe = (VariableElement) ele;

                // 获取该元素封装类型
                TypeElement enclosingElement = (TypeElement) varELe
                        .getEnclosingElement();

                // 拿到key
                String key = enclosingElement.getQualifiedName().toString();
                fields = maps.get(key);
                if (fields == null) {
                    maps.put(key, fields = new ArrayList<VariableElement>());
                }
                fields.add(varELe);
            }
        }

        for (String key : maps.keySet()) {
            if (maps.get(key).size() == 0) {
                TypeElement typeElement = elementUtils.getTypeElement(key);
                List<? extends Element> allMembers = elementUtils
                        .getAllMembers(typeElement);
                if (allMembers.size() > 0) {
                    maps.get(key).addAll(ElementFilter.fieldsIn(allMembers));
                }
            }
        }
        generateCodes(maps);
        return true;
    }

    private void generateCodes(Map<String, List<VariableElement>> maps) {
        File dir = new File("d://apt_test");
        if (!dir.exists())
            dir.mkdirs();

        // 遍历map
        for (String key : maps.keySet()) {

            // 创建文件
            File file = new File(dir, key.replaceAll("\\.", "_") + ".json");
            try {

                /**
                 * 编写json文件内容
                 */

                FileWriter fw = new FileWriter(file);
                fw.append("{").append("class:").append("\"" + key + "\"")
                        .append(",\n ");
                fw.append("fields:\n {\n");
                List<VariableElement> fields = maps.get(key);
                for (int i = 0; i < fields.size(); i++) {
                    VariableElement field = fields.get(i);
                    fw.append("  ").append(field.getSimpleName()).append(":")
                            .append("\"" + field.asType().toString() + "\"");
                    if (i < fields.size() - 1) {
                        fw.append(",");
                        fw.append("\n");
                    }
                }
                fw.append("\n }\n");
                fw.append("}");
                fw.flush();
                fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
     * @return  注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
     */

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(Person.class.getCanonicalName());
        return annotataions;
    }

    /**
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
     * @return  使用的Java版本
     */

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

上面注释说的挺清楚了,我们需要处理的工作在 process() 方法中进行,等下给出例子。对于 getSupportedAnnotationTypes() 方法标明了这个注解处理器要处理哪些注解,返回的是一个Set 值,说明一个注解处理器可以处理多个注解。除了在这个方法中指定要处理的注解外,还可以通过注解的方式来指定(SourceVersion也一样):

@SupportedSourceVersion(SourceVersion.RELEASE_8)  
@SupportedAnnotationTypes("com.example.Person")  
public class PersonProcessor extends AbstractProcessor {  
    // ...  
}

因为兼容的原因,特别是针对Android平台,建议使用重载 getSupportedAnnotationTypes() 和 getSupportedSourceVersion()方法代替@SupportedAnnotationTypes 和@SupportedSourceVersion,本例中就采用的使用重载 getSupportedAnnotationTypes() 和 getSupportedSourceVersion()方法。

运行注解处理器

在运行前,你需要在主项目工程中引入 annotations 和 processors 这两个库(引入 processors 库不是个好做法,后面介绍更适当的方法)。这时如果你直接编译或者运行工程的话,是看不到任何输出信息的,这里还要做的一步操作是指定注解处理器的所在,需要做如下操作:

  1. 在 processors 库的 main 目录下新建 resources 资源文件夹;

  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;

  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;

  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

来看下整个目录结构:

这里写图片描述

然后我们打开annotations和processors两个Java库中的build.gradle文件,将sourceCompatibility、targetCompatibility都设置为1.8。

处理完就可以使用了,我们在项目中定义两个类,并使用 @Person 注解类的变量和类:

public class Student {
    @Person
    private String name;
    private String age;
    @Person
    private String address;
}

@Person
public class Teacher {
    private String score;
    private String article_count;
}

到这里我们使用“Make Project”编译工程就应该有文件生成,如果没看到文件生成,则先执行“Clean Project”清理下工程再编译,就有文件生成了,生成的文件如下:

这里写图片描述

AutoService

前面在指定注解处理器的时候会很麻烦,那么多步骤就为添加一个注解处理器,不过没关系,AutoService 可以帮你解决这个问题。AutoService注解处理器是Google开发的,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的,你只需要在你定义的注解处理器上添加 @AutoService(Processor.class) 就可以了,非常方便。

先给 processors 库依赖上 AutoService,你可以直接在 AndroidStudio 工具上搜索添加,如下:

compile 'com.google.auto.service:auto-service:1.0-rc2'

添加好以后就可以直接用了,在我们之前定义的注解处理器上使用:

@AutoService(Processor.class)  
public class PersonProcessor extends AbstractProcessor {  
    // ...  
} 

这时重新Make下工程也能看到同样的文件生成了。但是如果你编译生成APK时,你会发现出现错误了,提示发现文件重复了!这里有个解决办法是在主项目的 build.gradle 加上这么一段:

apply plugin: 'com.android.application'  

android {  
    // ...  
    packagingOptions {  
        exclude 'META-INF/services/javax.annotation.processing.Processor'  
    }  
} 

这样就不会报错了,这是其中的一个解决方法,还有个更好的解决方法就是用上上面提到的android-apt了。

android-apt

那么什么是android-apt呢?官网有这么一段描述:

The android-apt plugin assists in working with annotation processors in combination with Android Studio. It has two purposes:

1、Allow to configure a compile time only annotation processor as a dependency, not including the artifact in the final APK or library

2、Set up the source paths so that code that is generated from the annotation processor is correctly picked up by Android Studio

大体来讲它有两个作用:

  • 能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留的东西

  • 能够辅助 Android Studio 在项目的对应目录中存放注解处理器在编译期间生成的文件

这个就可以很好地解决上面我们遇到的问题了,来看下怎么用。

首先在整个工程的 build.gradle 中添加如下两段语句:

buildscript {  
    repositories {  
        jcenter()  
        mavenCentral()  // add  
    }  
    dependencies {  
        classpath 'com.android.tools.build:gradle:2.1.2'  
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  // add  
    }  
} 

在主项目(app)的 build.gradle 中也添加两段语句:

apply plugin: 'com.android.application'  
apply plugin: 'com.neenbedankt.android-apt' // add  
// ...  
dependencies {  
    compile fileTree(include: ['*.jar'], dir: 'libs')  
    testCompile 'junit:junit:4.12'  
    compile 'com.android.support:appcompat-v7:23.4.0'  
    compile project(':annotations')  
    //compile project(':processors')  替换为下面  
    apt project(':processors')  
}  

这样就OK了,重新运行可以很好地工作了~

点击此处获取例程源码

参考文献

【1】《Java和Android中的注解》
http://blog.csdn.net/CShawnX/article/details/51605919

【2】 《Java注解(Annotation)》
http://gityuan.com/2016/01/23/java-annotation/

【3】《关于java编译时注解你需要知道的二三事。解除你的顾虑!》
http://blog.csdn.net/liu470368500/article/details/51316066

【4】《自定义注解之编译时注解(RetentionPolicy.CLASS)(一)》
http://blog.csdn.net/github_35180164/article/details/52121038

【5】Android 打造编译时注解解析框架 这只是一个开始
http://blog.csdn.net/lmj623565791/article/details/43452969

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值