欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

(2.1.19.3)深入理解Java:注解(Annotation)–编译时注解的处理

程序员文章站 2022-06-17 11:55:10
...

一、注解

Annotations是一种元数据,其作用在于提供程序本身以外的一些数据信息,也就是说Annotation他不会属于程序代码本身,不参与逻辑运算,故而不会对原程序代码的操作产生直接的影响。

一般来说Annotation有如下三种使用情形:

  • 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. SOURCE 标记一些信息,为编译器提供辅助信息。

    • 可以为编译器提供而外信息,以便于检测错误,抑制警告等,譬如@Override、@SuppressWarnings等这类注解就是用于标识,可以用作一些检验。
  2. CLASS 编译时动态处理。

    • 一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的,也就是在类加载的时候丢弃。
    • 会依靠动态生成的类做一些操作,因为没有反射,效率和直接调用方法没什么区别。ParcelableGenerator、butterknife 、androidannotaion都使用了类似技术
  3. RUNTIME 运行时动态处理。

    • 这个大家见得应该最多,在运行时拿到类的Class对象,然后遍历其方法、变量,判断有无注解声明,然后做一些事情。
    • 譬如使用表单验证注解@Validate,不保留活动的@SaveInstance

来看一个示例:

@Retention(RetentionPolicy.CLASS)  
@Target({ ElementType.FIELD, ElementType.TYPE })  
public @interface InjectView  
{  
    int value();  
}  
  • @Retention后面的值 标示注解的类型
  • @Target的值是一个ElementType[]数组,标明这个注解能标识哪些东西,比如类、变量、方法、甚至是注解本身(元注解)等

更多信息见 (2.1.19)注释与注解

在本篇中我们主要讲述的就是如何对 CLASS类的编译期注解的处理

二、注解处理器

在开始之前,我们再次申明一个非常重要的问题:我们并不讨论那些在运行时(Runtime)通过反射机制运行处理的注解,而是讨论在编译时(Compile time)处理的注解。

注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器。到这里,我假设你已经知道什么是注解,并且知道怎么申明的一个注解类型。

一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。

这具体的含义什么呢?

你可以生成Java代码!这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译

三、虚处理器AbstractProcessor

我们首先看一下处理器的API。每一个处理器都是继承于AbstractProcessor,如下所示:

package com.example;

public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    @Override
    public SourceVersion getSupportedSourceVersion() { }

}
  • init(ProcessingEnvironment env): 【核心】

    • 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。
    • ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。后面我们将看到详细的内容
      Filer是个接口,支持通过注解处理器创建新文件
      Elements 元素操作辅助工具类
  • process(Set< ? extends TypeElement> annotations, RoundEnvironment env):【核心】

    • 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
    • 输入参数annotations 请求处理的注解类型集合
    • 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素,相当于“有关全局源码的上下文环境”。后面我们将看到详细的内容。
    • @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
  • getSupportedAnnotationTypes()

    • 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
    • 换句话说,你在这里定义你的注解处理器注册到哪些注解上。
  • getSupportedSourceVersion()

    • 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。
    • 然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者

在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion(),像这样:

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({// 合法注解全名的集合})
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}

除此以外还有一个@SupportedOptions,这个一般是命令行时候用的,设置一些选项,but,命令行我不熟,因此:略。
注:如果大家找不到AbstractProcessor,记得右键build-path add library把jdk加进来

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

接下来的你必须知道的事情是,注解处理器是运行它自己的虚拟机JVM中。

是的,你没有看错,javac启动一个完整Java虚拟机来运行注解处理器。这对你意味着什么?你可以使用任何你在其他java应用中使用的的东西。使用guava。如果你愿意,你可以使用依赖注入工具,例如dagger或者其他你想要的类库。但是不要忘记,即使是一个很小的处理,你也要像其他Java应用一样,注意算法效率,以及设计模式

3.1 处理器对全局代码的扫描处理流程

注解处理器对工程代码的扫描是多次的,可以注意到AbstractProcessor的process()方法的输入参数有一个是RoundEnvironment,这个代表一次扫描的结果

影响注解处理器执行顺序与逻辑的地方有三处

3.1.1 javax.annotation.processing.Processor中的书写顺序决定注册处理器的执行顺序

假设该文件中定义如下:

package.ProcessorB
package.ProcessorA

那么编译器每一轮扫描会先执行处理器ProcessorB,再执行处理器ProcessorA

3.1.2 AbstractProcessor中processor()方法的返回值决定是否要终结当前轮的处理

按照1中注册的顺序,假如ProcessorB中的process()方法返回true,则表示消费完这轮的注解扫描,将不再执行ProcessorA,只有当返回false时,才会接下来执行ProcessorB

3.1.3 没有输出文件跟输入文件时扫描结束

假设按照1中的注册顺序,ProcessorB中的process()方法返回true,并且ProcessorB在第一轮扫描会生成按文件GenerateB.java,则将在第三轮扫描后结束注解处理

也就是说针对生成的文件,还是再处理一次

过程如下

过程 输入文件 输出文件
第一轮 原工程 GenerateB.java
第二轮 GenerateB.java
第三轮

第三轮时编译器发现没有输出文件也没有输入文件,处理结束
PS:每个注解处理器在整个过程中都保持同一个实例

四 语言模型包的使用

4.1 Mirror

注解处理器因为操作的是源码,所以需要用到JAVA语言模型包,javax.lang.model及其子包都是Java的语言模型包。这个包是采用了Mirror设计,Java是一种可以自描述的语言,其反射机制就是一种自描述,传统的反射机制将自描述与其他操作合并在一起,Mirror机制将自描述跟其他操作隔离,自描述部分是Meta level,其他部分是Base level

(2.1.19.3)深入理解Java:注解(Annotation)–编译时注解的处理

4.2 Element详解

有了注解,必然需要有一个对应的注解处理器去处理注解,但是在处理注解的时候需要充分的了解注解处理器中的process方法及时核心的编译代码,而process方法的核心便是Element对象,所以在讲解注解处理器前,需要对Element有全面的认识,方能事半功倍。

Element在逻辑上代表语言元素,比如包,类,方法等

其实Element是定义的一个接口,定义了外部调用暴露出的接口

方法 解释
TypeMirror asType(); 返回此元素定义的类型,实际的java类型
ElementKind getKind(); 返回此元素的种类:包、类、接口、方法、字段…,如下枚举值,方法返回一个枚举值TypeKind
Set getModifiers() 返回此元素的修饰符,如下枚举值
Name getSimpleName(); 返回此元素的简单名称,比如activity名,变量就是变量名
Element getEnclosingElement(); 返回封装此元素的最里层元素,即最近的外层元素,如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素; 如果此元素是顶层类型,则返回它的包如果此元素是一个包,则返回 null; 如果此元素是一个泛型参数,则返回 null.
List getEnclosedElements(); 获取所有的内层元素
< A extends Annotation> A getAnnotation(Class< A> var1); 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的

4.2.1 转化

Element代表语言元素,比如包,类,方法等,但是Element并没有包含自身的信息,自身信息要通过Mirror来获取,每个Element都指向一个TypeMirror,这个TypeMirror里有自身的信息。通过下面获方法取Element中的Mirror

TypeMirror mirror=element.asType() 

TypeMirror类型是DeclaredType或者TypeVariable时候可以转化成Element

Types types=processingEnvironment.getTypeUtils()
Element element=types.asElement(typeMirror)

4.2.2 获取类型getKind()

获取Element或者TypeMirror的类型都是通过getKind()获取类型,但是返回值虽然不同,但是都是枚举。

ElementKind element.getKind()
TypeKind typeMirror.getKind()

针对具体类型,要避免用instanceof,因为即使同一个类getKind()也有不同的结果。比如TypeElemnt的getKind()返回结果可以是枚举,类,注解,接口四种。

enum ElementKind {
     PACKAGE,
     ENUM,
     CLASS, //类
     ANNOTATION_TYPE,
     INTERFACE, //接口
     ENUM_CONSTANT,
     FIELD, //普通的成员变量
     PARAMETER, //函数或构造函数的参数
     LOCAL_VARIABLE,//本地变量
     EXCEPTION_PARAMETER,
     METHOD,//函数
     CONSTRUCTOR,//构造函数
     STATIC_INIT,
     INSTANCE_INIT,
     OTHER,
     RESOURCE_VARIABLE;

}

enum TypeKind {
     BOOLEAN,
     BYTE,
     SHORT,
     INT,
     LONG,
     CHAR,
     FLOAT,
     DOUBLE,
     VOID,
     NONE,
     NULL,
     DECLARED, 
     ERROR,
     TYPEVAR,
     WILDCARD,
     PACKAGE,
     EXECUTABLE,
     OTHER,
     UNION,
     INTERSECTION;
}

4.2.3 Element子类

Element在逻辑上代表语言元素,比如包,类,方法等,因此也会有五个直接子接口,它们分别代表一种特定类型的元素

Tables Are
PackageElement 一个包程序元素
TypeElement 一个类或接口程序元素
ExecutableElement 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素
TypeParameterElement 一般类、接口、方法或构造方法元素的泛型参数
VariableElement 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

其实也正好对应了注解的@Target的作用域(详见 (2.1.19)注释与注解):

  • packages:ElementType.PACKAGE
  • types(类、接口、枚举、Annotation类型):ElementType.TYPE
  • 类型成员(方法、构造方法、成员变量、枚举值):ElementType.METHOD
  • 方法参数和本地变量(如循环变量、catch参数):ElementType.PARAMETER,ElementType.FIELD,ElementType.LOCAL_VARIABLE

五个子类各有各的用处并且有各种独立的方法,在使用的时候可以强制将Element对象转换成其中的任一一种,但是前提是满足条件的转换,不然会抛出异常

4.2.3.1 TypeElement详解

TypeElement定义的一个类或接口程序元素,相当于当前注解所在的class对象

方法 解释
NestingKind getNestingKind(); 返回此类型元素的嵌套种类
Name getQualifiedName(); 返回此类型元素的完全限定名称。更准确地说,返回规范 名称。对于没有规范名称的局部类和匿名类,返回一个空名称.譬如 Activity就是包名+类名
TypeMirror getSuperclass(); 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类 java.lang.Object,则返回一个种类为 NONE 的 NoType
List getInterfaces(); 返回直接由此类实现或直接由此接口扩展的接口类型
List getTypeParameters(); 按照声明顺序返回此类型元素的形式类型参数

4.2.3.2 VariableElement详解

VariableElement标示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

方法 解释
getConstantValue 变量初始化的值
getEnclosingElement 获取相关类信息

4.3 源码中移动

比如一个类里面有方法,成员变量等,这个类相对于方法跟成员变量就是外层元素,而成员变量和方法相对于类就是内层元素。
Element是代表源码的类,源码中移动到其他位置必须是用Element,比如移动到当前元素的外层元素TypeElement

public static TypeElement findEnclosingTypeElement( Element e ){
       while( e != null && !(e instanceof TypeElement) )       {
           e = e.getEnclosingElement();//通过getEnclosingElement()获取外层元素
       }
       return TypeElement.class.cast( e );
}

也可以使用getEnclosedElements()获取当前Element内层的所有元素

4.4 类继承树结构中移动

TypeMirror是用来反应类本身信息的类,在继承树移动必须用到TypeMirror,比如查找某个类的父类

Types typeUtils = processingEnv.getTypeUtils();
while (!typeElement.toString().equals(Object.class.getName())) {
        TypeMirror typeMirror=element.getSuperclass();
        element = (TypeElement)typeUtils.asElement(typeMirror);
}

4.5 处理MirroredTypeException

编译器使用注解处理器来处理目标代码时候,目标源码还没有编译成字节码,所以任何指向目标源码字节码的代码都会发生MirroredTypeException。最常见的例子见下面

//已经编译的三方jar:
@Retention(RetentionPolicy.SOURCE)
@interface What{
    Class<?> value()
}

//源码:
@What(A.class)
public class A{}

//注解处理器
public ProcessorA extends AbstractProcessor{
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        ```
        What what=typeElementA.getAnnotation(What.class);
        Class<?> clazz=what.value();  //这里将有MirroredTypeException
        ```
    }
}

注解处理器之所有MirroredTypeException,是因为此时类A还没有被编译成字节码,所以A.class不存在,解决这个问题需要异常,利用异常我能获取类A的名字等信息,但是却得不到A.class,如下

public ProcessorA extends AbstractProcessor{
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        ```
        What what=typeElementA.getAnnotation(What.class);
        try {
            Class<?> clazz=what.value();  //这里将有MirroredTypeException
        }catch(MirroredTypeException mte){
            TypeMirror classTypeMirror =  mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement)Types.asElement(classTypeMirro);
            //获取canonicalName
            String canonicalName= classTypeElement.getQualifiedName().toString();
            //获取simple name
            String simpleName = classTypeElement.getSimpleName().toString();
        }
       // ```
    }
}

五、示例

5.1 打印Element信息

以上的部分是不是挺懵逼?~~ 赶紧来个简单示例吧

//注解
// SOURCE也能在编译期获得,用于打印信息等
@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe {
}

//注解处理器
@SupportedAnnotationTypes({"com.avenwu.annotation.PrintMe"})
public class MyProcessor extends AbstractProcessor {
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {//annotations当前处理器支持的所有注解,env全局源代码环境
        Messager messager = processingEnv.getMessager();
        for (TypeElement te : annotations) {//遍历 “当前处理器支持的所有注解”
            for (Element e : env.getElementsAnnotatedWith(te)) {//获得被该注解声明的元素  
                messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.toString()); //Gradle Console的打印
            }
        }
        return true;
    }

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

//注解使用
@PrintMe
public class MainActivity extends Activity{

 //```

 @PrintMe
 public void showDrawerFrame(```){
    //```
 }
}

Gradle Console的输出为:

(2.1.19.3)深入理解Java:注解(Annotation)–编译时注解的处理

5.2 生成json描述文件

定义Seriable 注解,
- 如果类上声明注解我们就将其所有的变量都生成一个json描述文件
- 如果仅仅是成员变量呢,那我们只提取声明的成员变量来动态生成

//目标1
public class User  
{  
    @Seriable  
    private String username;  
    @Seriable  
    private String password;  

    private String three;  
    private String four;  
} 

//目标二
@Seriable  
public class Article  
{  
    private String title ;   
    private String content ;   
}  

//预期结果1:com_zhy_User文件
{class:"com.zhy.User",  
 fields:  
 {  
  username:"java.lang.String",  
  password:"java.lang.String"  

 }  
} 

//预期结果2:com_zhy_Article文件
{class:"com.zhy.Article",  
 fields:  
 {  
  content:"java.lang.String",  
  title:"java.lang.String"  
 }  
}  

基本分为两个过程:
1. 找出标识注解的类或成员变量,封装到maps中;
2. 遍历maps为每个类创建json文件。
- 我们把文件输出到了f://apt_test文件夹中,如果你没有f盘神马的,自行修改目录

@SupportedAnnotationTypes("com.zhy.annotationprocess.annotation.Seriable")  
@SupportedSourceVersion(SourceVersion.RELEASE_6)  
public class BeanProcessor extends AbstractProcessor  
{ // 元素操作的辅助类  
    Elements elementUtils;  

    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv)  
    {  
        super.init(processingEnv);  
        // 元素操作的辅助类  
        elementUtils = processingEnv.getElementUtils();  
    }  

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

        // 【1】获得被该注解声明的所有元素  
        Set<? extends Element> elememts = roundEnv  
                .getElementsAnnotatedWith(Seriable.class);  
        TypeElement classElement = null;// 声明类元素  
        List<VariableElement> fields = null;// 声明一个存放成员变量的列表  
        // 存放二者   类名:成员变量元素
        Map<String, List<VariableElement>> maps = new HashMap<String, List<VariableElement>>();  
        // 【2】遍历 ,找出标识注解的类或成员变量,封装到maps中;
        for (Element ele : elememts)  
        {  
            // 【2.1】判断该元素是否为类  
            if (ele.getKind() == ElementKind.CLASS)  
            {  
                classElement = (TypeElement) ele;  
                maps.put(classElement.getQualifiedName().toString(),  
                        fields = new ArrayList<VariableElement>()); //向maps中加入一个映射对   类名:变量元素的空容器 

            } else if (ele.getKind() == ElementKind.FIELD) // 【2.2】判断该元素是否为成员变量  
            {  
                VariableElement varELe = (VariableElement) ele;  
                // 获取该元素封装类型,即外层元素

                TypeElement enclosingElement = (TypeElement) varELe  
                        .getEnclosingElement();  //【2.2.1】获取最近的外层类,其实就是Class对象
                // 拿到key  
                String key = enclosingElement.getQualifiedName().toString();  
                fields = maps.get(key);  
                if (fields == null)  
                {  
                    maps.put(key, fields = new ArrayList<VariableElement>());  
                }  
                fields.add(varELe);  //【2.2.2】 向map的key=Class对象名所对应的Map中加入当前变量
            }  
        }  

        //【2.3】 遍历Maps,如果key所对应的容器容量为0,则证明是Seriable注解被加入到了类上,应该全类记录
        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));  //过滤成员变量元素,并加入到对应的list中
                }  
            }  
        }  
        //【3】遍历maps为每个类创建json文件
        generateCodes(maps);  

        return true;  
    }  

    private void generateCodes(Map<String, List<VariableElement>> maps)  
    {  
        File dir = new File("f://apt_test");  
        if (!dir.exists())  
            dir.mkdirs();  
        // 遍历map  
        for (String key : maps.keySet())  
        {  

            // 创建文件  
            File file = new File(dir, key.replaceAll("\\.", "_") + ".txt");  
            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();  
            }  

        }  
    }  

}  

5.3 butterknife的 @BindView

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_msg)
    TextView tvMsg;
    @BindView(R.id.tv_other)
    TextView tvOther;
}
  • 先判断Element类型,如果是VariableElement则继续获取相关的包名(这里必须在app包名一致,不然获取不到android类)类对象信息,以及@BindView注解修饰的参数数据;
  • 最后将所有需要的数据通过javapoet和Filer自动编译创建一个java文件

最终输出:

package com.wzgiceman.viewinjector;

import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivityViewInjector implements ViewInjector<MainActivity> {

  @Override
  public void inject(MainActivity host, Object object) {
     if(object instanceof android.app.Activity){
     host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
     }
    else{
     host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
    }
  }
}

处理器代码:

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        //一、收集信息
        for (Element element : elements) {
            /*检查类型*/
            if (!(element instanceof VariableElement)) {//BindView仅会作用在成员变量上,也可以使用ele.getKind() == ElementKind.FIELD判断
                return false;
            }
            VariableElement variableElement = (VariableElement) element;

            /*获取类信息*/
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            /*类的绝对路径*/
            String qualifiedName = typeElement.getQualifiedName().toString();
            /*类名*/
            String clsName = typeElement.getSimpleName().toString();
            /*获取包名*/
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();

            /*获取注解*/
            BindView annotation = variableElement.getAnnotation(BindView.class);
            int id = annotation.value();

            /*参数名*/
            String name = variableElement.getSimpleName().toString();
            /*参数对象类*/
            String type = variableElement.asType().toString();

            ClassName InterfaceName = ClassName.bestGuess("com.example.annotation.api.ViewInjector");
            ClassName host = ClassName.bestGuess(qualifiedName);

            MethodSpec main = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(host, "host")
                    .addParameter(Object.class, "object")
                    .addCode(""
                            + " if(object instanceof android.app.Activity){\n"
                            + " host." + name + " = (" + type + ")(((android.app.Activity)object).findViewById(" + id + "));\n"
                            + " }\n"
                            + "else{\n"
                            + " host." + name + " = (" + type + ")(((android.view.View)object).findViewById(" + id + "));\n"
                            + "}\n")
                    .build();

            TypeSpec helloWorld = TypeSpec.classBuilder(clsName + "ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(main)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
                    .build();

            try {
                // 生成 com.example.HelloWorld.java
                JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .build();
                // 生成文件
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }

需要注意的是,上述代码中,再一次遍历后就进行了IO操作,这也就导致了只初始化了tvMsg对象,tvOther并没有初始化

所以常规的做法应该是,统一扫描,统一处理,尤其注意重复数据的剔除

六、注册你的处理器

在自定义完注解处理器后,必须将它注册到javac中,才会在编译期间调用处理器对注解进行处理。

6.1 Android studio

1.注解器模块配置如下

apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
dependencies {
  compile 'com.google.auto.service:auto-service:1.0-rc2'
}

2.android studio会自动生成为javax.annotation.processing.Processor,只需为注解器添加@AutoService注解,这样就可以自动生成META-INF内的文件,如下

@AutoService(Processor.class)
public class ProcessorA extends AbstractProcessor{
    ```
}
  1. 工程的build.gradle:
dependencies {
     // 其他classpath
     classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  //添加apt插件
 }
  1. 调用模块的build.gradle:
applying plugin: 'com.android.application'  
apply plugin: 'com.neenbedankt.android-apt' //使用apt插件
dependencies {
    apt '注解处理器.jar'  //注解处理器
    compile '关于处理器生成文件的调用包.jar'   //API包
}

或者

直接使用annotationProcessor就可以标示注解处理器

 annotationProcessor project(':arouter-compiler')
 // annotationProcessor 'com.alibaba:arouter-compiler:1.0.3'

6.2 eclipse

  1. 你必须提供一个.jar文件。就像其他.jar文件一样,你打包你的注解处理器到此文件中
  2. 在你的jar中,你需要打包一个特定的文件”javax.annotation.processing.Processor”到META-INF/services路径下
    • 打包进MyProcessor.jar中的javax.annotation.processing.Processor的内容是,注解处理器的合法的全名列表,每一个元素换行分割
  3. 把MyProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册MyProcessor作为注解处理器

所以,你的.jar文件看起来就像下面这样:

MyProcessor.jar
    com
        example
            MyProcessor.class
    META-INF
        services
            javax.annotation.processing.Processor

其中内容为

com.example.MyProcessor  
com.foo.OtherProcessor  
net.blabla.SpecialProcessor

七、其他

7.1 编译期的日志打印

注解处理器的打印日志全部应该交由Messager处理,并且需要在打印错误时利用异常抛出来终止处理器

Messager messager=processingEnv.getMessager()
//打印非error级别log
messager.printMessage(Kind.NOTE,message);

//打印error时抛出异常来中断注解处理器
messager.printMessage(Kind.ERROR,error);
throw new RuntimeException(error);

7.2 javapoet代码生成辅助工具库

JavaPoet一个是创建 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,GitHub上面有非常详细的用法,建议好好阅读相关的使用

processors依赖:

compile 'com.squareup:javapoet:1.8.0'

来看一个简单示例:

    // 创建main方法
    MethodSpec main = MethodSpec.methodBuilder("main")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(void.class)
            .addParameter(String[].class, "args")
            .addStatement("$T.out.println($S)", System.class, clsNmae+"-"+msg)
            .build();

    // 创建HelloWorld类
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addMethod(main)
            .build();

    try {
        // 生成 com.wzgiceman.viewinjector.HelloWorld.java
        JavaFile javaFile = JavaFile.builder("com.wzgiceman.viewinjector", helloWorld)
                .addFileComment(" This codes are generated automatically. Do not modify!")
                .build();
        // 生成文件
        javaFile.writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }

运行后,在build文件夹的class文件夹下可以看到:

【其他2】
(2.1.19.3)深入理解Java:注解(Annotation)–编译时注解的处理

7.3 android-apt编译期文件冲突解决

编译时注解的自动写入也会导致代码混乱,可能在多次build编译过程中出现文件冲突的情况,所以这里需要引入android-apt

android-apt能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留无用的文件,辅助 Android Studio项目的对应目录中存放注解处理器在编译期间生成的文件

依赖使用:

//根目录build.gradle
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

//app中
apply plugin: 'com.neenbedankt.android-apt'
apt project(':processors') //这里是apt替换compile依赖processors

7.4 代码结构

编译时注解越来越多的出现在各大开源框架使用中,比如

  • JakeWharton/butterknife view
  • greenrobot/EventBus 事件
  • square/dagger 依赖注入

编译时注解框架在编写时有相对固定的格式,分包为例

【其他1】
(2.1.19.3)深入理解Java:注解(Annotation)–编译时注解的处理

其中除了app是Android moudel以外,其他全部均是Java moudel

  • 依赖关系
    • annotations 无
    • processors 依赖 api || annotations
    • app依赖有 api && annotations && processors

参考文献