Android修炼系列(五),写一篇超全面的annotation讲解

2,896 阅读16分钟

不学注解,也许是因为平时根本不需要没事自定义个这玩意玩,可随着Android形势越来越内卷,不学点东西是真不行了。而通过本文的学习,可以让你对于注解有个全面的认识,你会发现,小小的注解,大有可为,编不下去了..

注解不同于注释,注释的作用是为了方便自己或者别人的阅读,能够利用 javadoc 提取源文件里的注释来生成人们所期望的文档,对于代码本身的运行是没有任何影响的。

而注解的功能就要强大很多,不但能够生成描述符文件,而且有助于减轻编写“样板”代码的负担,使代码干净易读。通过使用扩展的注解(annotation)API 我们能够在 编译期运行期 对代码进行操控。

注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后的某个时刻非常方便的使用这些数据。 —Jeremy Meyer

本文主要对于下面几个方面进行讲解,篇幅很长,建议收藏查看:

Java 最初内置的三种标准注解

注解是 java SE5中的重要的语言变化之一,你可能对注解的原理不太理解,但你每天的开发中可能无时无刻不在跟注解打交道,最常见的就是 @Override 注解,所以注解并没有那么神秘,也没有那么冷僻,不要害怕使用注解(虽然使用的注解大部分情况都是根据需要自定义的注解),用的多了自然就熟了。为什么说最初的三种标准注解呢,因为在后续的 java 版本中又陆陆续续的增加了一些注解,不过原理都是一样的。

java SE5内置的标准注解含义
@Override表示当前的方法定义将覆盖超类中的方法,如果方法拼写错误或者方法签名不匹配,编译器便会提出错误提示
@Deprecated表示当前方法已经被弃用,如果开发者使用了注解为它的元素,编译器便会发出警告信息
@SuppressWarnings可以关闭不当的编译器警告信息

Java 提供的四种元注解和一般注解

所谓元注解(meta-annotation)也是一种注解,只不过这种注解负责注解其他的注解。所以再说元注解之前我们来看一下普通的注解:

public @interface LogClassMessage {}

这是一个最普通的注解,注解的定义看起来很像一个接口,在 interface 前加上 @ 符号。事实上在语言级别上,注解也和 java 中的接口、类、枚举是同一个级别的,都会被编译成 class 文件。而前面提到的元注解存在的目的就是为了修饰这些普通注解,但是要明确一点,元注解只是给普通注解提供了作用,并不是必须存在的。

java 提供的元注解作用
@Target定义你的注解应用到什么地方(详见下文解释)
@Retention定义该注解在哪个级别可用(详见下文解释)
@Documented将此注解包含在 javadoc 中
@Inherited允许子类继承超类中的注解

〔1〕@Target使用的时候添加一个 ElementType 参数,表示当前注解可以应用到什么地方,即可以指定一种,也可以同时指定多种,使用方法如下:

    // 表示当前的注解只能应用到类、接口(包括注解)、enum上面
    @Target(ElementType.TYPE) 
    public @interface LogClassMessage {}
    // 表示当前的注解只能应用到方法和成员变量上面
    @Target({ElementType.METHOD,ElementType.FIELD})
    public @interface LogClassMessage {}

下面来看一下 ElementType 的全部参数含义:

ElementType 参数说明
ElementType.CONSTRUCTOR构造器的声明
ElementType.FIELD域的声明(包括enum的实例)
ElementType.LOCATION_VARLABLE局部变量的声明
ElementType.METHOD方法的声明
ElementType.PACKAGE包的声明
ElementType.PARAMETER参数的声明
ElementType.TYPE类、接口(包括注解类型)、enum声明

〔2〕@Retention用来注解在哪一个级别可用,需要添加一个 RetentionPolicy 参数,用来表示在源代码中(SOURCE),在类文件中(CLASS)或者运行时(RUNTIME):

    // 表示当前注解运行时可用
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LogClassMessage {}

下面来看一下 RetentionPolicy 的全部参数含义:

RetentionPolicy 参数说明
RetentionPolicy.SOURCE注解将被编译器丢弃,只能存于源代码中
RetentionPolicy.CLASS注解在class文件中可用,能够存于编译之后的字节码之中,但会被VM丢弃
RetentionPolicy.RUNTIMEVM在运行期也会保留注解,因此运行期注解可以通过反射获取注解的相关信息

在注解中,一般都会包含一些元素表示某些值,并且可以为这些元素设置默认值,没有元素的注解也称为标记注解(marker annotation)

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD,ElementType.FIELD})
    public @interface LogClassMessage {
        public int id () default -1;
        public String message() default "";
    }

注:虽然上面的 id 和 message 定义和接口的方法定义很类似,但是在注解中将 id 和 message 称为:int 元素 id , String 元素 message。而且注解元素的类型是有限制的,并不是任何类型都可以,主要包括:基本数据类型(理论上是没有基本类型的包装类型的,但是由于自动封装箱,所以也不会报错)、String 类型、enum 类型、Class 类型、Annotation 类型、以及以上类型的数组,(没有等字,说明目前注解的元素类型只支持上面列出的这几种),否则编译器便会提示错误。

invalid type 'void ' for annotation member // 例如注解类型为void的错误信息

对于默认值限制 ,Bruce Eckel 在其书中是这样描述的:编译器对元素的默认值有些过分挑剔,首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供注解的值。其次,对于非基本类型的元素,无论在源代码声明中,或者在注解接口中定义默认值时,都不能以 null 作为其值。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有元素都存在,并且都具有相应的值。为了绕开这个约束,我们只能自己定义一些特殊的值,例如空字符串或者负数,以此表示某个元素的不存在,这算得上是一个习惯用法。

参考系统的标准注解

怎么说呢,接触一种知识的途径有很多,可能每一种的结果都是大同小异的,都能让你学到东西,但是实现的方式、实现过程中的规范、方法和思路却并不一定是最佳的。

上文讲到的是注解的基本语法,那么系统是怎么用的呢?首先让我们来看一下使用频率最高的 @Override :

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

〔1〕首先系统定义一个没有元素的标记注解 Override ,随后使用元注解 @Target 指明 Override 注解只能应用于方法之上(你可以细想想,是不是在我们实际使用这个注解的时候,只能是重写方法,没有见过重写类或者字段的吧),使用注解 @Retention 表示当前注解只能存在源代码中,并不会出现在编译之后的 class 文件之中。

    @Override
    protected void onResume() {
        super.onResume();
    }

〔2〕如在 Activity 中我们可以重写 onResume() 方法,添加注解 @override 之后编译器便会去检查父类中是否存在相同方法,如果不存在便会报错。

〔3〕也许到这里你会感到很疑惑,注解到底是怎么工作的,怎么系统这样定义一个注解 Override 它就能工作了?黑魔法吗,擦擦,完成看不到实现过程嘛(泪流满面),经过查阅了一些资料(非权威)了解到,其实处理过程都编写在了编译器里面,也就是说编译器已经给我们写好了处理方法,当编译器进行检查的时候就会调用相应的处理方法。

注解处理器

介绍之前,先引用 Jeremy Meyer 的一段话:如果没有用来读取注解的工具,那么注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5 扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具 apt帮助程序员解析带有注解的 java 源代码。

根据上面描述我们可以知道,注解处理器并不是一个特定格式,并不是只有继承了 AbstractProcessor 这个抽象类才叫注解处理器,凡是根据相关API 来读取注解的类或者方法都可以称为注解处理器。

反射机制下的处理器

最简单的注解处理器莫过于,直接使用反射机制的 getDeclaredMethods 方法获取类上所有方法(字段原理是一样的),再通过调用 getAnnotation 获取每个方法上的特定注解,有了注解便可以获取注解之上的元素值,方法如下:

    public void getAnnoUtil(Class<?> cl) {
        for(Method m : cl.getDeclaredMethods()) {
            LogClassMessage logClassMessage = m.getAnnotation(LogClassMessage .class);
            if(null != logClassMessage) {
                int id = logClassMessage.id();
                String method = logClassMessage.message();
            }
        }
    }

由于反射对性能会有一定的损耗,所以上述类型的注解处理器并不占主流,现在使用最多的还是 AbstractProcessor 自定义注解处理器,因为后者并不需要通过反射实现,效率和直接调用普通方法没有区别,这也是为什么编译期注解比运行时注解更受欢迎。

但是并不是说为了性能运行期注解就不能用了,只能说不能滥用,要在性能方面给予考虑。目前主要的用到运行期注解的框架差不多都有缓存机制,只有在第一次使用时通过反射机制,当再次使用时直接从缓存中取出。

好了,说着说着就跑题,还是来聊一下这个 AbstractProcessor 类吧,到底有何魅力让这么多人为她沉迷,方法如下:


public class MyFirstProcessor extends AbstractProcessor {

    /**
     * 做一些初始化工作,注释处理工具框架调用了这个方法,给我们传递一个 ProcessingEnvironment 类型的实参。
     *
     * <p>如果在同一个对象多次调用此方法,则抛出IllegalStateException异常。
     *
     * @param processingEnvironment 这个参数里面包含了很多工具方法
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {

        // 返回用来在元素上进行操作的某些工具方法的实现
        Elements es = processingEnvironment.getElementUtils();
        // 返回用来创建新源、类或辅助文件的Filer
        Filer filer = processingEnvironment.getFiler();
        // 返回用来在类型上进行操作的某些实用工具方法的实现
        Types types = processingEnvironment.getTypeUtils();
        
        // 这是提供给开发者日志工具,我们可以用来报告错误和警告以及提示信息
        // 注意 message 使用后并不会结束过程,Kind 参数表示日志级别
        Messager messager = processingEnvironment.getMessager();
        messager.printMessage(Diagnostic.Kind.ERROR, "例如当默认值为空则提示一个错误");
        // 返回任何生成的源和类文件应该符合的源版本
        SourceVersion version = processingEnvironment.getSourceVersion();

        super.init(processingEnvironment);
    }

    /**
     * @return 如果返回true 不要求后续Processor处理它们,反之,则继续执行处理。
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        /**
         * TypeElement 这表示一个类或者接口元素集合常用方法不多,TypeMirror getSuperclass()返回直接超类。
         * 
         * <p>详细介绍下 RoundEnvironment 这个类,常用方法:
         * boolean errorRaised() 如果在以前的处理round中发生错误,则返回true
         * Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)
         * 这里的 a 即你自定义的注解class类,返回使用给定注解类型注解的元素的集合
         * Set<? extends Element> getElementsAnnotatedWith(TypeElement a)
         * 
         * <p>Element 的用法:
         * TypeMirror asType() 返回此元素定义的类型 如int
         * ElementKind getKind() 返回元素的类型 如 e.getkind() = ElementKind.FIELD 字段
         * boolean equals(Object obj) 如果参数表示与此元素相同的元素,则返回true
         * Name getSimpleName() 返回此元素的简单名称
         * List<? extends Elements> getEncloseElements 返回元素直接封装的元素
         * Element getEnclosingElements 返回此元素的最里层元素,如果这个元素是个字段等,则返回为类
         */

        return false;
    }

    /**
     * 指出注解处理器 处理哪种注解
     * 在 jdk1.7 中,我们可以使用注解 {@SupportedAnnotationTypes()} 代替
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    /**
     * 指定当前注解器使用的Jdk版本。
     * 在 jdk1.7 中,我们可以使用注解{@SupportedSourceVersion()}代替
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}

自定义运行期注解(RUNTIME)

我们在开发中经常会需要计算一个方法所要执行的时间,以此来直观的比较哪个实现方式最优,常用方法是开始结束时间相减

System.currentTimeMillis()

但是当方法多的时候,是不是减来减去都要减的怀疑人生啦,哈哈,那么下面我就来写一个运行时注解来打印方法执行的时间。

1.首先我们先定义一个注解,并给注解添加我们需要的元注解:

/**
 * 这是一个自定义的计算方法执行时间的注解。
 * 只能作用于方法之上,属于运行时注解,能被VM处理,可以通过反射得到注解信息。
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CalculateMethodRunningTime {

    // 要计算时间的方法的名字
    String methodName() default "no method to set";
}

2.利用反射方法在程序运行时,获取被添加注解的类的信息:

public class AnnotationUtils {

    // 使用反射通过类名获取类的相关信息。
    public static void getClassInfo(String className) {
        try {
            Class c = Class.forName(className);
            // 获取所有公共的方法
            Method[] methods = c.getMethods();
            for (Method m : methods) {
                Class<CalculateMethodRunningTime> ctClass = CalculateMethodRunningTime.class;
                if (m.isAnnotationPresent(ctClass)) {
                    CalculateMethodRunningTime anno = m.getAnnotation(ctClass);
                    // 当前方法包含查询时间的注解时
                    if (anno != null) {
                        final long beginTime = System.currentTimeMillis();
                        m.invoke(c.newInstance(), null);
                        final long time = System.currentTimeMillis() - beginTime;
                        Log.i("Tag", anno.methodName() + "方法执行所需要时间:" + time + "ms");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.在 activity 中使用注解,注意咱们的注解是作用于方法之上的:

public class ActivityAnnotattion extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_anno);
        AnnotationUtils.getClassInfo("com.annotation.zmj.annotationtest.ActivityAnnotattion");
    }

    @CalculateMethodRunningTime(methodName = "method1")
    public void method1() {
        long i = 100000000L;
        while (i > 0) { i--; }
    }

}

4.运行结果:

这里写图片描述

自定义编译期注解(CLASS)

为什么要最后说编译期注解呢,因为相对前面的自定义注解来说,编译期注解有些难度,涉及到的东西比较多,但却是平时用到的最多的注解,因为编译期注解不存在反射,所以对性能没有影响。

本来也想用绑定 View 的例子讲解,但是现在这样的 demo 网上各种泛滥,而且还有各路大牛写的,所以我就没必要班门弄斧了。在这里以跳转界面为例:

    Intent intent = new Intent (this, NextActivity.class);
    startActivity (intent);

本着方便就是改进的原则,让我们定义一个编译期注解,来自动生成上述的代码,想想每次需要的时候只需要一个注解就能跳转到想要跳转的界面是不是很刺激。

1.首先新建一个 android 项目,在创建两个 java module(File -> New -> new Module ->java Module),因为有的类在android项目中不支持,建完后项目结构如下:

这里写图片描述

其中 annotation 中盛放自定义的注解,annotationprocessor 中创建注解处理器并做相关处理,最后的 app 则为我们的项目。

注意:MyFirstProcessor类为上文讲解 AbstractProcessor 所建的类,可以删去,跟本项目没有关系。

2.处理各自的依赖

annotation

processor

app

3.编写自定义注解,这是一个应用到字段之上的注解,被注解的字段为传递的参数

/**
 * 这是一个自定义的跳转传值所用到的注解。
 * value 表示要跳转到哪个界面activity的元素,传入那个界面的名字。
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface IntentField {
    String value () default " ";
}

4.自定义注解处理器,获取被注解元素的类型,进行相应的操作。

@AutoService(javax.annotation.processing.Processor.class)
public class MyProcessot extends AbstractProcessor{

    private Map<Element, List<VariableElement>> items = new HashMap<>();
    private List<Generator> generators = new LinkedList<>();

    // 做一些初始化工作
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        Utils.init();
        generators.add(new ActivityEnterGenerator());
        generators.add(new ActivityInitFieldGenerator());
    }

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

        // 获取所有注册IntentField注解的元素
        for (Element elem : roundEnvironment.getElementsAnnotatedWith(IntentField.class)) {
            // 主要获取ElementType 是不是null,即class,interface,enum或者注解类型
            if (elem.getEnclosingElement() == null) {
                // 直接结束处理器
                return true;
            }

            // 如果items的key不存在,则添加一个key
            if (items.get(elem.getEnclosingElement()) == null) {
                items.put(elem.getEnclosingElement(), new LinkedList<VariableElement>());
            }

            // 我们这里的IntentField是应用在一般成员变量上的注解
            if (elem.getKind() == ElementKind.FIELD) {
                items.get(elem.getEnclosingElement()).add((VariableElement)elem);
            }
        }

        List<VariableElement> variableElements;
        for (Map.Entry<Element, List<VariableElement>> entry : items.entrySet()) {
            variableElements = entry.getValue();
            if (variableElements == null || variableElements.isEmpty()) {
                return true;
            }
            // 去通过自动javapoet生成代码
            for (Generator generator : generators) {
                generator.genetate(entry.getKey(), variableElements, processingEnv);
                generator.genetate(entry.getKey(), variableElements, processingEnv);
            }
        }
        return false;
    }

    // 指定当前注解器使用的Java版本
    @Override public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    // 指出注解处理器 处理哪种注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>(2);
        annotations.add(IntentField.class.getCanonicalName());
        return annotations;
    }
}

5.这是一个工具类方法,提供了本 demo 中所用到的一些方法,其实实际里面的方法都很常见,只不过做了一个封装而已.

public class Utils {

    private static Set<String> supportTypes = new HashSet<>();

    /** 当getIntent的时候,每种类型写的方式都不一样,所以把每种方式都添加到了Set容器中。*/
    static void init() {
        supportTypes.add(int.class.getSimpleName());
        supportTypes.add(int[].class.getSimpleName());
        supportTypes.add(short.class.getSimpleName());
        supportTypes.add(short[].class.getSimpleName());
        supportTypes.add(String.class.getSimpleName());
        supportTypes.add(String[].class.getSimpleName());
        supportTypes.add(boolean.class.getSimpleName());
        supportTypes.add(boolean[].class.getSimpleName());
        supportTypes.add(long.class.getSimpleName());
        supportTypes.add(long[].class.getSimpleName());
        supportTypes.add(char.class.getSimpleName());
        supportTypes.add(char[].class.getSimpleName());
        supportTypes.add(byte.class.getSimpleName());
        supportTypes.add(byte[].class.getSimpleName());
        supportTypes.add("Bundle");
    }

    /** 获取元素所在的包名。*/
    public static String getPackageName(Element element) {
        String clazzSimpleName = element.getSimpleName().toString();
        String clazzName = element.toString();
        return clazzName.substring(0, clazzName.length() - clazzSimpleName.length() - 1);
    }


    /** 判断是否是String类型或者数组或者bundle,因为这三种类型getIntent()不需要默认值。*/
    public static boolean isElementNoDefaultValue(String typeName) {
        return (String.class.getName().equals(typeName) || typeName.contains("[]") || typeName.contains("Bundle"));
    }

    /**
     * 获得注解要传递参数的类型。
     * @param typeName 注解获取到的参数类型
     */
    public static String getIntentTypeName(String typeName) {
        for (String name : supportTypes) {
            if (name.equals(getSimpleName(typeName))) {
                return name.replaceFirst(String.valueOf(name.charAt(0)), String.valueOf(name.charAt(0)).toUpperCase())
                        .replace("[]", "Array");
            }
        }
        return "";
    }

    /**
     * 获取类的的名字的字符串。
     * @param typeName 可以是包名字符串,也可以是类名字符串
     */
    static String getSimpleName(String typeName) {
        if (typeName.contains(".")) {
            return typeName.substring(typeName.lastIndexOf(".") + 1, typeName.length());
        }else {
            return typeName;
        }
    }


    /** 自动生成代码。*/
    public static void writeToFile(String className, String packageName, MethodSpec methodSpec, ProcessingEnvironment processingEnv, ArrayList<FieldSpec> listField) {
        TypeSpec genedClass;
        if(listField == null) {
            genedClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(methodSpec).build();
        }else{
            genedClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(methodSpec)
                    .addFields(listField).build();
        }
        JavaFile javaFile = JavaFile.builder(packageName, genedClass)
                .build();
        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

6.自定义一个接口,把需要自动生成的每个java文件的方法都独立出去。

public interface Generator {
    void genetate(Element typeElement
            , List<VariableElement> variableElements
            , ProcessingEnvironment processingEnv);

}

7.编写自动生成文件的格式,生成后的类格式如下:

跳转类格式

上图为本例中的MainActivity$Enter类,如果你想生成一个类,那么这个类的格式和作用肯定已经在你的脑海中有了定型,如果你自己都不知道想要生成啥,那还玩啥。

/**
 * 这是一个要自动生成跳转功能的.java文件类
 * 主要思路:1.使用javapoet生成一个空方法
 *         2.为方法加上实参
 *         3.方法的里面的代码拼接
 * 主要需要:获取字段的类型和名字,获取将要跳转的类的名字
 */
public class ActivityEnterGenerator implements Generator{

    private static final String SUFFIX = "$Enter";

    private static final String METHOD_NAME = "intentTo";

    @Override
    public void genetate(Element typeElement, List<VariableElement> variableElements,  ProcessingEnvironment processingEnv) {
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class);
        // 设置生成的METHOD_NAME方法第一个参数
        methodBuilder.addParameter(Object.class, "context");
        methodBuilder.addStatement("android.content.Intent intent = new android.content.Intent()");

        // 获取将要跳转的类的名字
        String name = "";

        // VariableElement 主要代表一般字段元素,是Element的一种
        for (VariableElement element : variableElements) {
            // Element 只是一种语言元素,本身并不包含信息,所以我们这里获取TypeMirror
            TypeMirror typeMirror = element.asType();
            // 获取注解在身上的字段的类型
            TypeName type = TypeName.get(typeMirror);
            // 获取注解在身上字段的名字
            String fileName = element.getSimpleName().toString();
            // 设置生成的METHOD_NAME方法第二个参数
            methodBuilder.addParameter(type, fileName);
            methodBuilder.addStatement("intent.putExtra(\"" + fileName + "\"," +fileName + ")");
            // 获取注解上的元素
            IntentField toClassName = element.getAnnotation(IntentField.class);
            String name1 = toClassName.value();
            if(null != name && "".equals(name)){
                name = name1;
            }
            // 理论上每个界面上的注解value一样,都是要跳转到的那个类名字,否则提示错误
            else if(name1 != null && !name1.equals(name)){
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "同一个界面不能跳转到多个活动,即value必须一致");
            }
        }
        methodBuilder.addStatement("intent.setClass((android.content.Context)context, " + name +".class)");
        methodBuilder.addStatement("((android.content.Context)context).startActivity(intent)");

        /**
         * 自动生成.java文件
         * 第一个参数:要生成的类的名字
         * 第二个参数:生成类所在的包的名字
         * 第三个参数:javapoet 中提供的与自动生成代码的相关的类
         * 第四个参数:能够为注解器提供Elements,Types和Filer
         */
        Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX, Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv,null);
    }

}

当我们定义了跳转的类,那么接下来肯定就是在另一个界面获取传递过来的数据了,参考格式如下,这是本demo中自动生成的MainActivity$Init 类。

获取参数格式

/**
 * 要生成一个.Java文件,在这个Java文件里生成一个获取上个界面传递过来数据的方法
 * 主要思路:1.使用Javapoet生成一个空的的方法
 *         2.为方法添加需要的形参
 *         3.拼接方法内部的代码
 * 主要需要:获取传递过来字段的类型
 */
public class ActivityInitFieldGenerator implements Generator {

    private static final String SUFFIX = "$Init";

    private static final String METHOD_NAME = "initFields";

    @Override
    public void genetate(Element typeElement, List<VariableElement> variableElements, ProcessingEnvironment processingEnv) {

        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
                .addModifiers(Modifier.PROTECTED)
                .returns(Object.class);

        final ArrayList<FieldSpec> listField = new ArrayList<>();

        if (null != variableElements && variableElements.size() != 0) {
            VariableElement element = variableElements.get(0);
            // 当前接收数据的字段的名字
            IntentField currentClassName = element.getAnnotation(IntentField.class);
            String name = currentClassName.value();

            methodBuilder.addParameter(Object.class, "currentActivity");
            methodBuilder.addStatement(name + " activity = (" + name + ")currentActivity");
            methodBuilder.addStatement("android.content.Intent intent = activity.getIntent()");
        }

        for (VariableElement element : variableElements) {

            // 获取接收字段的类型
            TypeName currentTypeName = TypeName.get(element.asType());
            String currentTypeNameStr = currentTypeName.toString();
            String intentTypeName = Utils.getIntentTypeName(currentTypeNameStr);

            // 字段的名字,即key值
            Name filedName = element.getSimpleName();

            // 创建成员变量
            FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(element.asType()),filedName+"")
                    .addModifiers(Modifier.PUBLIC)
                    .build();
            listField.add(fieldSpec);

            // 因为String类型的获取 和 其他基本类型的获取在是否需要默认值问题上不一样,所以需要判断是哪种
            if (Utils.isElementNoDefaultValue(currentTypeNameStr)) {
                methodBuilder.addStatement("this."+filedName+"= intent.get" + intentTypeName + "Extra(\"" + filedName + "\")");
            } else {
                String defaultValue = "default" + element.getSimpleName();
                if (intentTypeName == null) {
                    // 当字段类型为null时,需要打印错误信息
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "the type:" + element.asType().toString() + " is not support");
                } else {
                    if ("".equals(intentTypeName)) {
                        methodBuilder.addStatement("this." + filedName + "= (" + TypeName.get(element.asType()) + ")intent.getSerializableExtra(\"" + filedName + "\")");
                    } else {
                        methodBuilder.addParameter(TypeName.get(element.asType()), defaultValue);
                        methodBuilder.addStatement("this."+ filedName +"= intent.get"
                                + intentTypeName + "Extra(\"" + filedName + "\", " + defaultValue + ")");
                    }
                }
            }
        }
        methodBuilder.addStatement("return this");
        Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX,  Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv, listField);
    }
}

8、在Activity中使用刚才的自定义注解。

public class MainActivity extends AppCompatActivity {

    @IntentField("NextActivity")
    int count = 10;
    @IntentField("NextActivity")
    String str = "编译器注解";
    @IntentField("NextActivity")
    StuBean bean = new StuBean(1,"No1");

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        addOnclickListener();
    }

    public void addOnclickListener() {
        findViewById(R.id.tvnext).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 从哪个界面进行跳转,则以哪个界面打头,enter 结尾
                // 例如 MainActivity$Enter
                new MainActivity$Enter()
                        .intentTo(MainActivity.this, count, str, bean);
            }
        });
    }
}

9.这是实体bean

public class StuBean implements Serializable{
    public StuBean(int id , String name) {
        this.id = id;
        this.name = name;
    }
    //学号
    public int id;
    //姓名
    public String name;
}

10、在NextActivity接收并打印数据:

public class NextActivity extends AppCompatActivity {

    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);
        textView = (TextView) findViewById(R.id.tv);
        
        // 想获取从哪个界面传递过来的数据,就已哪个类打头,init结尾
        // 例如 MainActivity$Init
        MainActivity$Init formIntent = (MainActivity$Init)new MainActivity$Init().initFields(this,0);
        textView.setText(formIntent.count + "---" + formIntent.str + "---" +formIntent.bean.name);
      
        // 打印上个界面传递过来的数据
        Log.i("Tag",formIntent.count + "---" + formIntent.str + "---" + formIntent.bean.name);
    }
}

11.运行结果:

这里写图片描述

总结

好了,看到这里,你应该对注解有所了解了,但是看的再懂也不如自己动手练一下。如果你仔细研究了,你会发现一个非常奇怪的事情,当我们设置 RetentionPolicy.CLASS 级别的时候,仍能通过反射获取注解信息,当我们设置 RetentionPolicy.SOURCE 级别的时候,仍能走通编译期注解,是不是非常迷惑。

之后只能又找了一些资料(非权威),看到了一个比较受认同的解释:这个属性主要给IDE 或者编译器开发者准备的,一般应用级别上不太会用到。

好了,本文到这里就结束了,关于注解的讲解应该非常全面了。

参考 1、B.E,Java编程思想:机械工业出版社