Android APT注解处理器

APT简介


Annotation Processing Tool ,即注解处理器。一般用来处理自定义的注解,然后根据注解生成一个辅助类。最著名的例子就是@BindView注解。

注意,这是在编译时扫描所以继承AbstractProcessor类,然后调用process方法去处理。因为是在编译的时候处理的,所以很多时候需要用到反射。

流程


  1. 创建一个java library库,用来提供注解
  2. 创建一个java library库,用来处理注解
  3. 在android app中引用

总体流程基本上分为这三个部分。但是有些时候,我们会在第2部到第3步之间创建一个android library库,然后在android app中直接引用这个android library

例子


初级

本例只看一下流程,对注解的处理比较粗糙。

  1. 创建java library库,命名为lib-anno,该库用来定义注解

    在该库中创建一个注解

    // 当前注解所在包为 com.hhh.lib_anno
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindView {
           
        int id();
    }
    
  2. 再创建一个java library库,命名为lib-processor,该库用来处理注解

    首先在该module的build.gradle文件中引入依赖

    apply plugin: 'java-library'
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        // 引入注解的库
        implementation project(':lib-anno')
    
        // AutoService 注解
        annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
        implementation 'com.google.auto.service:auto-service-annotations:1.0-rc6'
    
        // 用于生成Java文件
        implementation 'com.squareup:javapoet:1.12.1'
    }
    
    sourceCompatibility = "8"
    targetCompatibility = "8"
    

    自定义Processor处理自定义的注解

    // 所在包 com.hhh.lib_processor
    @AutoService(Processor.class)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @SupportedAnnotationTypes({
           
            "com.hhh.lib_anno.BindView"
    })
    public class MyProcessor extends AbstractProcessor {
           
    
        /**
         * @param set 需要处理的注解。也就是SupportedAnnotationTypes注解里面的内容
         */
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
           
            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                    "开始处理注解了啦啦啦啦啦 ");
    
            // 获取所有被BindView注解的元素
            Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            if (set == null) {
           
                // 直接报错
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                        "没找到任何元素");
            }
    
            for (Element e : bindViewElements) {
           
                // 判断被BindView注解的元素是否是全局变量
                if (e.getKind() == ElementKind.FIELD) {
           
                    VariableElement variableElement = (VariableElement) e;
    
                    // 输出被注解的元素
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                            "variableElement : "+variableElement);
    
                    BindView annotation = variableElement.getAnnotation(BindView.class);
                    int id = annotation.id();
                    // 输出注解的id
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                            "id : "+id);
                }
            }
            return true;
        }
    }
    

    当前process方法中仅仅是输出被BindView注解的元素以及对应的id,没有做其他处理

    • @AutoService

      google开源库,用来进行组件化开发的。如果没有该注解,可以自己手动注册processor

    • @SupportedSourceVersion

      提供支持的java版本号。如果没有该注解,需要重写getSupportedSourceVersion方法

    • SupportedAnnotationTypes

      提供需要处理的注解。如果没有该注解,需要重写getSupportedAnnotationTypes方法

  3. 在android app中使用

    在模块的build.gradle引入注解库,并配置注解处理库

    // 在dependcies方法里面引入
    
    // 引入注解库
    implementation project(':lib-anno')
    // apt 配置注解处理库
    annotationProcessor project(':lib-processor')
    

    在Activity中使用注解

    public class MainActivity extends AppCompatActivity {
           
    
        //R.id.tv是在xml文件中的TextView的id
        @BindView(id = R.id.tv)
        public TextView mTextView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
           
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    

好吧,三个步骤完成了,最后build一下,输出如下所示:

警告: 开始处理注解了啦啦啦啦啦 
警告: variableElement : mTextView
警告: id : 2131165359

可以看到,输出被注解的元素mTextView 以及id。

但是这个注解有什么用?好吧,没用,就是看个流程,接下来重新

进阶

接下来,将在AbstractProcessor处理BindView注解,将被BindView注解的View自动初始化,即做以下处理

mTextView = findViewById(R.id.tv);

但是要实现上面这一句咋办?生成一个辅助类呗,然后将对应的Activity传入进去,最终生成的类要类似这样的:

// 所在包 com.hhh.aptdemo
public class MainActivity$Helper implements IInjection {
     
  @Override
  public void inject(Object obj) {
     
    MainActivity a = (MainActivity) obj;
    a.mTextView = a.findViewById(2131165359);
  }
}

其中,IInjection 是自己定义的一个接口,方便对外提供。MainActivity名称是动态变化的,如果在其他Activity中,就需要换成其他Activity的名字。

  1. 在lib-anno中创建IInjection接口。要求所有被BindView注解元素所在类生成的辅助类都要继承它

    package com.hhh.lib_anno;
    
    public interface IInjection {
           
        void inject(Object activity);
    }
    
  2. 重新写个Procesoor在app下使用


    public class MainActivity extends AppCompatActivity {

    //R.id.tv是在xml文件中的TextView的id
    @BindView(id = R.id.tv)
    public TextView mTextView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MainActivity$Helper helper = new MainActivity$Helper();
        helper.inject(this);
    
        mTextView.setText(" hello world ,this is test");
    }
    

    }

  3. @AutoService(Processor.class)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @SupportedAnnotationTypes({
           
            "com.hhh.lib_anno.BindView"
    })
    public class MyProcessor extends AbstractProcessor {
           
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
           
            // 获取所有被BindView注解的元素
            Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            if (set == null) {
           
                // 直接报错
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                        "没找到任何元素");
            }
    
            // 用来存储Activity以及Activity中被BindView注解的元素
            Map<TypeElement, Set<ViewInfo>> map = new HashMap<>();
    
            for (Element e : bindViewElements) {
           
                // 判断被BindView注解的元素是否是全局变量
                if (e.getKind() == ElementKind.FIELD) {
           
    
                    // 被注解的元素
                    VariableElement variableElement = (VariableElement) e;
    
                    // 获取有BindView注解的元素的最直接外层
                    // 例如在一个类A的全局变量B使用了注解,那么最直接外层元素就是类A
                    Element enclosingElement = variableElement.getEnclosingElement();
    
                    // 判断当前封装了BindView注解的最里层元素是否是类
                    if (enclosingElement.getKind() == ElementKind.CLASS) {
           
                        TypeElement classEle = (TypeElement) enclosingElement;
    
                        // 填充集合,将BindView注解的View的名称与id一一对应起来,保存为ViewInfo
                        Set<ViewInfo> viewInfos = map.get(classEle);
                        if (viewInfos == null) {
           
                            viewInfos = new HashSet<>();
                            map.put(classEle, viewInfos);
                        }
                        BindView annotation = variableElement.getAnnotation(BindView.class);
                        ViewInfo info = new ViewInfo(variableElement.getSimpleName().toString(), annotation.id());
                        viewInfos.add(info);
                    }
                }
            }
    
            createFile(map);
            return true;
        }
    
        private void createFile(Map<TypeElement, Set<ViewInfo>> map) {
           
            for (TypeElement typeElement : map.keySet()) {
           
    
                // 获取到类。即含有BindView注解元素的类。本例子中就是MainActivity
                Name simpleName = typeElement.getSimpleName();
    
                // 创建代码块
                CodeBlock.Builder builder = CodeBlock.builder()
                        // 强制转换为对应的类
                        .addStatement("$N a = ($N) obj", typeElement.getSimpleName(), typeElement.getSimpleName());
                for (ViewInfo info : map.get(typeElement)) {
           
                    builder.addStatement("a.$L = a.findViewById($L)", info.viewName, info.id);
                }
    
                // 创建参数
                ParameterSpec objPara = ParameterSpec.builder(Object.class, "obj")
                        .build();
    
                // 创建方法
                MethodSpec injectMethod = MethodSpec.methodBuilder("inject")
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC)
                        .returns(void.class)
                        .addParameter(objPara)
                        .addCode(builder.build())
                        .build();
    
                // 创建类
                TypeSpec helperClass = TypeSpec.classBuilder(simpleName.toString() + "$Helper")
                        .addModifiers(Modifier.PUBLIC)
                        .addSuperinterface(IInjection.class)
                        .addMethod(injectMethod)
                        .build();
    
                System.out.println(typeElement.getQualifiedName());
    
    
                // 获取当前类所在的包
                PackageElement packageEle = processingEnv.getElementUtils().getPackageOf(typeElement);
    
                // 创建java文件
                JavaFile javaFile = JavaFile.builder(packageEle.getQualifiedName().toString(), helperClass).build();
    
                try {
           
                    javaFile.writeTo(processingEnv.getFiler());
                } catch (IOException e) {
           
                    e.printStackTrace();
                }
            }
        }
    }
    
  4. 创建一个Android Library的module

    该module的作用就是为了想外提供api,隐藏生成的辅助类,不要app直接访问辅助类。

    首先,build.gradle文件中,需要引用 IInjection 所在的库

    implementation project(':lib-anno')
    

    再创建一个类,用来提供api

    public class Registrar {
           
    
        private Registrar() {
           
    
        }
    
        private static class SingleHolder {
           
            public static final Registrar INSTANCE = new Registrar();
        }
    
        public static Registrar getInstance() {
           
            return SingleHolder.INSTANCE;
        }
    
        public void register(Activity activity) {
           
            // 通过反射获取Activity的辅助类,然后创建实例,在
            try {
           
                Class<?> clazz = Class.forName(activity.getClass().getName() + "$Helper");
                // 因为规定所有的Helper类都继承了IInjection,所以直接强制转换
                IInjection iInjection = (IInjection) clazz.newInstance();
                iInjection.inject(activity);
            } catch (Exception e) {
           
                e.printStackTrace();
            }
        }
    }
    

    这里直接通过反射来获取生成的Helper类,在使用的时候根本不需要知道生成的Helper类的名称以及所在地址

  5. 在app中使用

    首先在build.gradle中引入common库

    // 引入common库
    implementation project(':lib-common')
    

    然后再Activity中使用注解,并注册

    public class MainActivity extends AppCompatActivity {
           
    
        //R.id.tv是在xml文件中的TextView的id
        @BindView(id = R.id.tv)
        public TextView mTextView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
           
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Registrar.getInstance().register(this);
    
            mTextView.setText(" hello world ,this is test");
        }
    }
    

    ok,完成了,可以看到mTextView并没有使用findViewById方法,直接可以使用注解来找到对应的id

其他


在使用apt的时候,有一些相关只是是需要了解的。

Element

元素,可以是方法,可以是类,也可是是属性。可以认为只要能被注解的东西都是元素

常用方法

  • getSimpleName:获取元素名称。比如元素是类,只会获取到类名称
  • getQualifiedName:获取元素全称。比如元素是类,会获取到包名和类
  • asType:返回元素定义的类型,返回结果为TypeMirror
  • getAnnotataion:返回注解。即用该在元素上的注解的类型
  • getKind:返回元素的类型。比如是类,方法还是局部变量等
  • getModifiers:返回该元素上的修饰符。例如,public,final,static…
  • getEnclosedElements:返回该元素封装的元素。比如一个类是个类,那么会返回他的直接子元素(因为在类上,所以会有个构造函数也会一并返回)
  • getEnclosingElement:返回封装该元素的最里层元素

子类

  • ExecutableElement:表示方法,构造方法,初始化器

  • PackageElement:表示包

  • TypeElement:表示类,接口

  • TypeParameterElement:表示类,接口,方法的泛型类型。

  • VariableElement:表示一个字段,enum常量,方法的参数,局部变量以及异常参数

子类中有些特殊的方法不在介绍,直接看文档

TypeMirror

该类中只有一个常用的方法,即getKind,返回类型为TypeKind

TypeKind的作用是判断元素的类型,与Element的getKind方法返回的类型是完全不同的,但是两者在作用上类似。

CodeBlock

创建代码块的,当然也可以自己用StringBuild一个一个字符的敲。

该类支持占位符,使用$符号作为占位符的前缀

占位符

  • $L :没有转义的字面值。可以是字符串,基本数据类型,类型声明,注解以及其他代码块
  • $N :代指的是一个名称。通常使用该占位符通过名称引用另一个声明,如调用方法名称,变量名称等。注意,要使用该占位符,必须要有名称,一般用在应用FieldSpec,MethodSpec,TypeSpec等,因为在声明这些的时候,都要输入一个名字
  • $S :将值转义为字符串,这个与L的区别就是会在值上面加上双引号
  • $T :类型引用
  • $$ :表示美元符号$
  • $W :表示空格或者换行
  • $Z :充当0的宽度空间
  • $> :增加缩进级别
  • $< :减小缩进级别
  • $[ :表示开始声明
  • $] :表示结束声明

其实常用到的也就是 L , L, LN, S , S, ST

描述基本元素

  • AnnotationSpec :用来创建注解
  • FieldSpec :用来创建字段
  • MethodSpec :用来创建构造函数或者方法
  • ParameterSpec :用来创建方法或者构造函数上的参数
  • TypeSpec :用来创建类,接口或者枚举类

你可能感兴趣的:(Android)