Android开发—APT注解处理器详解

关于APT

APT(Annotation Processing Tool)是一种注解处理工具,它会对源文件进行扫描找出相应的Annotation并在注解处理器中进行操作,具体操作由注解处理器也就是用户自己去实现,比如可以生成一些新的文件或者其他文件等,最终会把新生成的文件和源文件一起进行编译。

APT工具常用的有2个,android-apt和Gradle2.2以后的annotationProcessor功能。

APT处理annotation的基本流程表示:

  • 定义注解,比如@Route
  • 自定义注解处理器,处理注解(如生成java文件等)
  • 使用注解处理器
APT操作@Route注解的大致步骤

android-apt

官方文档

一个Gradle插件帮助Android Studio处理annotation processors,Gradle2.2以后Gradle提供annotationProcessor的功能可以完全代替android-apt,android-apt官网上作者也说明了,不再维护,并且谷歌明确表示Gradle 3.0.0+ 不再支持 android-apt 插件,所以推荐使用annotationProcessor。

android-apt主要有2个目的:
1、允许在注解处理器编译的时候当做依赖,但是在打包apk或者当做类库的时候不会打到里面;
2、设置生成的资源路径以便能被Android studio正确访问到;

使用插件的时候如下配置gradle脚本

buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        // replace with the current version of the Android plugin
        classpath 'com.android.tools.build:gradle:1.3.0'
        // the latest version of the android-apt plugin
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

传递编译的参数

apt {
    arguments {
            resourcePackageName android.defaultConfig.applicationId
            androidManifestFile variant.outputs[0]?.processResources?.manifestFile
    }
}

由于android-apt已经过时了,并且annotationProcessor也正式被Google扶正,所以具体apt的使用不在进行演示,有兴趣的同学可以访问android-apt主页进行学习。

annotationProcessor

官方文档
annotationProcessor是Gradle2.2+内置的功能,不需要额外引入其他插件,可以向下面这样直接在gradle文件引入。

dependencies {
    // Adds libraries defining annotations to only the compile classpath.
    compileOnly 'com.google.dagger:dagger:version-number'
    // Adds the annotation processor dependency to the annotation processor classpath.
    annotationProcessor 'com.google.dagger:dagger-compiler:version-number'
}

这是引用第三方的注解处理器,我们实际开发中可以自定义注解处理器,下面我们自定义一个简单的注解处理器。

自定义注解处理器

自定义注解处理器的话需要用到2个第三方库AutoServiceJavaPoet ,还有Java自带的AbstractProcessor。

  • AbstractProcessor:Java内置注解处理器,注解处理器核心工作都在这个类进行。
  • AutoService:Google开源用来自动注册我们自己的注解处理器。
  • JavaPoet:Java代码生成器,方便我们生成Java文件;

我们按照上文说的APT处理annotation的基本流程来自定义。

1、定义注解,比如@Route
新建项目,然后新建一个Java module,叫annotationLib,里面定义我们自己的注解,关于注解的相关知识这里不再细说具体可以参考这里

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@interface Route {
    String value() ;
}

2、自定义注解处理器,处理注解(如生成java文件等)
再新建一个Java module,叫annotationCompiler,里面实现具体的注解处理器,配置gradle文件引入 AutoServiceJavaPoet,再依赖上我们之前定义的注解模块。

apply plugin: 'java-library'

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.squareup:javapoet:1.8.0'
    implementation project(':annotationLib')
}
sourceCompatibility = "7"
targetCompatibility = "7"

新建一个注解处理器继承自AbstractProcessor:

@AutoService(Processor.class)
public class RouteProcessor extends AbstractProcessor {
    private Messager messager;
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
    }

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

    public void loggerInfo(String msg) {
        messager.printMessage(Diagnostic.Kind.NOTE, msg);
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set != null && !set.isEmpty()) {
            loggerInfo("process start");
            StringBuilder printInfo = new StringBuilder();
            Set<? extends Element> routeElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
            try {
                if (routeElements != null && routeElements.size() > 0) {
                    printInfo.append(routeElements.size() + "个文件加了@Route注解!");
                }
            } catch (Exception e) {
                loggerInfo(e.getMessage());
            }
            //构建参数
            ParameterSpec msg = ParameterSpec.builder(String.class, "msg")
                    .build();
            //构建方法
            MethodSpec method = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(void.class)
                    .addParameter(msg)
                    .addStatement("$T.out.println($S+msg)", System.class, printInfo.toString())
                    .build();
            //构建类
            TypeSpec helloWorld = TypeSpec.classBuilder("InjectHelper")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(method)
                    .build();
             //构建文件并指定生成文件目录
            JavaFile javaFile = JavaFile.builder("com.wzh.annotation", helloWorld)
                    .build();
            loggerInfo("process end");
            try {
                //把类、方法、参数等信息写入文件
                javaFile.writeTo(filer);
            } catch (IOException e) {
                loggerInfo("process exception");
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }
}

然后我们在我们app模块引用这个注解编译器及注解,如下:

dependencies {
     ...
    annotationProcessor project(':annotationCompiler')
    implementation project(':annotationLib')
    ...
}

我们clean一下项目,然后rebuild一下,会发现如下目录生成的文件:

app/build/generated/source/apt/debug/com/wzh/annotation/InjectHelper.java

import java.lang.String;
import java.lang.System;

public final class InjectHelper {
  public static void inject(String msg) {
    System.out.println("2个文件加了@Route注解!"+msg);
  }
}

这就是我们生成的文件,很简单一个InjectHelper类,里面一个静态方法inject,打印出加@Route的文件个数。

3、使用注解处理器

@Route("main")
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectHelper.inject("调用生成类的方法");
    }
}
@Route("test")
public class TestClass {
     //empty class
}

这里我们演示很简单,在Activity上加了@Route的注解,并调用生成类的方法,然后在任意类都可以加注解,因为我们没做任何注解类的限制,运行程序输出:

System.out: 2个文件加了@Route注解!调用生成类的方法

Kotlin使用注解处理器

首先app模块引入注解处理器的时候需要引入kapt插件,在app下的gradle配置如下:

apply plugin: 'kotlin-kapt'
....
dependencies {
     ...
     //自定义注解处理器 module
    kapt project(':annotationCompiler')
    //自定义注解 module
    implementation project(':annotationLib')
    ...
}

其他配置基本一样,文件生成的目录变化,apt目录变为kapt

app/build/generated/source/kapt/debug/com/wzh/annotation/InjectHelper.java

给注解处理器传参数

在编译之前可以传递需要的参数给注解处理器,我们在app模块gradle传递module的名字给注解处理器:

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                 //参数名 route_module_name,携带的数据就是当前module的名字
                arguments = [route_module_name: project.getName()]
            }
        }
    }
}

在注解处理器init方法里接受参数:

private String moduleName = null;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    ...
    moduleName = processingEnvironment.getOptions().get("route_module_name");
    loggerInfo("moduleName = " + moduleName);
}

Rebuild项目的时候我们会在build控制台看到如下输出信息:

moduleName = app

APT的相关知识学习

自定义AbstractProcessor的时候我们会重写以下的方法:

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    // 初始化操作
}
@Override
public Set<String> getSupportedAnnotationTypes() {
    // 设置注解处理器需要处理的注解类型
}
@Override
public SourceVersion getSupportedSourceVersion() {
     //指定java版本
     return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //注解处理的核心方法
}

下面重点介绍initprocess方法

  • init(ProcessingEnvironment processingEnvironment) 方法
    此方法会被注解处理工具调用,参数ProcessingEnvironment 提供了一些实用的工具类Elements、Types和Filer等,如下表所示。
工具方法 功能
getElementUtils() 返回实现Elements接口的对象,用于操作元素的工具类
getFiler() 返回实现Filer接口的对象,用于创建文件、类和辅助文件
getMessager() 返回实现Messager接口的对象,用于报告错误信息、警告提醒
getOptions() 返回指定的参数选项,可在Gradle文件配置
getTypeUtils() 返回实现Types接口的对象,用于操作类型的工具类
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)方法
    此方法里面是我们进行注解处理逻辑的地方。
    参数1 Set<? extends TypeElement> set:返回所有当前注解处理器需要处理的Annotation.
    参数2 RoundEnvironment roundEnvironment:表示当前或是之前的运行环境,可以通过该对象查找到注解。

从roundEnvironment我们可以获取到Element被注解的元素信息。下面我们写个实例来打印一下看看。

package com.wzh.annotation;
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD,
ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE, ElementType.ANNOTATION_TYPE,ElementType.TYPE_PARAMETER})
public @interface Test {
    String value();
}

这里定义的注解为了方便打印,支持注解到类、方法、变量、参数等。下面使用注解。

package com.wzh.annotation;
@Test("this is class TestClass")
public class TestClass<T> implements TestInterface{
    @Test("this is local field name")
    private String name = "my name is test";

    @Test("this is local method sayHello")
    private String sayHello(@Test("this is parameter msg") String msg){
        String hello = "my name is hello";
        return hello;
    }
}

然后在注解处理器去打印元素信息。

private void parseTestAnnotation(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Test.class);
        for (Element element : elements){ //遍历所有元素
            if(element.getKind().equals(ElementKind.PACKAGE)){
                LoggerInfo("element--------------PACKAGE-------------------------------");
            } else if (element.getKind().equals(ElementKind.CLASS)){
                //被注解的元素是类
                TypeElement typeElement = (TypeElement) element;
                LoggerInfo("element--------------CLASS-------------------------------");
                //实现接口信息
                LoggerInfo("element:Interfaces = "+typeElement.getInterfaces().toString());
                //泛型参数
                LoggerInfo("element:TypeParameters = "+typeElement.getTypeParameters().toString());

                //element的父元素是包元素
                PackageElement packageElement = (PackageElement) element.getEnclosingElement();
                LoggerInfo("element:packageElement = "+packageElement.getQualifiedName());

            } else if (element.getKind().equals(ElementKind.FIELD)){
                //被注解的元素是全局变量
                LoggerInfo("element--------------FIELD-------------------------------");
                VariableElement variableElement = (VariableElement) element;
                //获取变量类型
                LoggerInfo("element:typeSimpleName = "+ types.asElement(variableElement.asType()).getSimpleName());
            } else if (element.getKind().equals(ElementKind.PARAMETER)){
                //被注解的元素是参数
                LoggerInfo("element--------------PARAMETER-------------------------------");
            } else if (element.getKind().equals(ElementKind.METHOD)){
                //被注解的元素是方法
                LoggerInfo("element--------------METHOD-------------------------------");
                ExecutableElement executableElement = (ExecutableElement) element;
                //获取方法的参数名
                LoggerInfo("element:Parameters = "+executableElement.getTypeParameters().toString());
                //获取方法的返回值类型
                LoggerInfo("element:ReturnType = "+executableElement.getReturnType().toString());
            }
            //打印注解里面的值
            LoggerInfo("element:value = "+ element.getAnnotation(Test.class).value());
            //打印包名信息
            LoggerInfo("element:packageName = "+ elementUtils.getPackageOf(element).getQualifiedName());
            //被注解元素的名称
            LoggerInfo("element:SimpleName = "+element.getSimpleName());
            //被注解元素的类型(String/int/float...)
            LoggerInfo("element:asType = "+element.asType().toString());
            //被注解元素的种类(PACKAGE、CLASS、METHOD、PARAMETER等)
            LoggerInfo("element:KindName = "+element.getKind().name());
            //获取父元素的种类(局部变量的父元素是方法、方法及全局变量的父元素是类、类元素的父元素是包)
            LoggerInfo("element:EnclosingElementKindName = "+element.getEnclosingElement().getKind().name());
            //被注解元素的修饰 如:public static 等
            LoggerInfo("element:Modifiers = "+element.getModifiers().toString());
        }
    }

注: >> element--------------CLASS-------------------------------
注: >> element:Interfaces = com.wzh.annotation.TestInterface
注: >> element:TypeParameters = T
注: >> element:packageElement = com.wzh.annotation
注: >> element:value = this is class TestClass
注: >> element:packageName = com.wzh.annotation
注: >> element:SimpleName = TestClass
注: >> element:asType = com.wzh.annotation.TestClass<T>
注: >> element:KindName = CLASS
注: >> element:EnclosingElementKindName = PACKAGE
注: >> element:Modifiers = [public]
注: >> element--------------FIELD-------------------------------
注: >> element:typeSimpleName = String
注: >> element:value = this is local field name
注: >> element:packageName = com.wzh.annotation
注: >> element:SimpleName = name
注: >> element:asType = java.lang.String
注: >> element:KindName = FIELD
注: >> element:EnclosingElementKindName = CLASS
注: >> element:Modifiers = [private]
注: >> element--------------METHOD-------------------------------
注: >> element:Parameters =
注: >> element:ReturnType = java.lang.String
注: >> element:value = this is local method sayHello
注: >> element:packageName = com.wzh.annotation
注: >> element:SimpleName = sayHello
注: >> element:asType = (java.lang.String)java.lang.String
注: >> element:KindName = METHOD
注: >> element:EnclosingElementKindName = CLASS
注: >> element:Modifiers = [private]
注: >> element--------------PARAMETER-------------------------------
注: >> element:value = this is parameter msg
注: >> element:packageName = com.wzh.annotation
注: >> element:SimpleName = msg
注: >> element:asType = java.lang.String
注: >> element:KindName = PARAMETER
注: >> element:EnclosingElementKindName = METHOD
注: >> element:Modifiers = []

由打印结果可以看到所有被注解的元素信息都被打印出来。

Element

Java文档关于Element介绍
Element代表一个程序元素,如包、类、方法、变量、参数、接口泛型等的接口,其有多种子类分别代表不同的程序元素,如:ExecutableElement 方法元素, PackageElement 包元素, TypeElement 类元素, TypeParameterElement 形参元素, VariableElement 变量及参数元素等。之前的TestClass<T>可以对应成下图。

Element对应图

TypeMirror

TypeMirror是一个接口,表示Java编程语言中的类型。这些类型包括基本类型、引用类型、数组类型、类型变量和null类型等等。Element的 asType()返回TypeMirror类型的值,我们通过这个值得getKind()方法获取元素的类型,这个类型有很多枚举类型如:CHARARRAY(数组)FLOATEXECUTABLE(方法)等。

总结

关于注解处理器具体使用还有很多东西,就不一一写出来了,具体可以参考JAVA API,不得不说注解处理器很强大,很多热门框架都使用了APT,如:butterknife、Arouter、Dagger2、EventBus等。所以学好注解处理器还是比较重要的,接下来我们实战一把,不看butterknife源码的情况下实现简单的功能。Android开发— APT之ButterKnife的简单功能实现

参考:
Java API
Java注解处理器
java-apt的实现之Element详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容