注解处理器(APT)了解一下

基本概念

APT 全称为 Annotation Processing Tool,可翻译为注解处理器,APT 工具是用于注解处理的命令行程序,它可以找到源码中对应注解的对象并使用注解处理器对其进行处理。
一般来说,我们会使用 APT 生成一些源码,然后加入编译目录进行编译,从而简化开发周期。

注解

注解处理器是基于注解(Annotation)的,实际开发中自定义注解用的比较少,这里先简单的复习下相关概念。
Java 注解是 Java1.5 中引入的概念,是用来标注代码的元数据。

定义一个注解

定义一个注解使用 @interface 关键字。

public @interface Test {}

对,这样一个名为 Test 的注解就定义好了。

元注解

元注解是注解的注解,用来标注注解的元数据。
可以通过元注解来控制注解的一些属性与行为。
Java 中元注解共有四种:Retention、Inherited、Documented、Target。

Retention

Retention 表示注解的保留范围,取值为枚举类:RetentionPolicy,其中一共包括如下三个类型。

  • SOURCE:注解只保留在源文件中,编译时就会把注解丢弃。
  • CLASS:默认值,注解保留到 class 文件中,运行时会丢弃。
  • RUNTIME:注解将会一直保留到运行时,可以通过反射获取到该注解。

Target

Target 用于控制注解的使用范围,取值为枚举类:ElementType。

  • TYPE:该注解作用于类,接口或枚举类上
  • FIELD:作用于类属性或对象属性也可以为枚举实例
  • METHOD:作用于函数
  • PARAMETER:作用于函数参数
  • CONSTRUCTOR:作用于构造器
  • PACKAGE:作用于包名
  • LOCAL_VARIABLE:作用于变量
  • ANNOTATION_TYPE:作用于注解
  • TYPE_PARAMETER:Java 1.8 引入,作用于类型参数例如:
@Target(ElementType.TYPE_PARAMETER)
public @interface Test {}

class TypeTest<@Test T>{}
  • TYPE_USE:Java 1.8 引入,作用于各种类型,也就是说,任何使用类型的地方都可以使用该注解。
@Target(ElementType.TYPE_USE)
public @interface Test {}

public class A {}

public class B extends @Test A {
    public @Test int add(@Test int a, @Test int b) {
        @Test int result = a + b;
        System.out.println(result);
        return result;
    }
}

Documented

Documented 用于描述该注解是否需要加入到例如 javadoc 工具生成的文档的公共 API 中,使用 Documented 的注解将会被保留在生成的文档中。

Inherited

使用 Inherited 元注解的注解修饰的类,子类将会继承这个注解,这么说有点绕,举个栗子。
首先定义注解 Test,且使用 @Inherited 元注解修饰。
然后定义一个 Father 类,使用 @Test 注解修饰,再定义一个 Son 类继承了 Father 类,那么 Son 也会拥有注解 Test。

OK,注解就说完了,回归正题,开始介绍 APT。

APT

APT 的原理就是在需要使用的元素上(类、变量、方法、参数等)加上我们的注解,然后在编译时把使用了这个注解的元素都收集起来,做统一的处理,例如根据元素生成对应的工具类,以此提高开发效率。

创建一个注解处理器分为如下几步:

  • 创建注解类
  • 创建一个继承自 AbstractProcessor 的类,这就是 APT 的核心类
  • 创建公开 API 及辅助工具
  • 创建配置文件
  • 使用

做 Android 开发的应该都知道 JakeWharton 大神的 ButterKnife 框架,这个框架就是通过 APT 技术实现的,不知道的也没关系,这个框架的功能就是通过注解给对象赋值。我们一般创建控件后还需要使用控件 ID 来获取控件的对象,控件多了之后比较麻烦,很多重复代码,这个框架就是直接通过控件 ID 来初始化控件的,我现在来模仿一下做个山寨版的 CopycatKnife 学习 APT。

原理就是我们根据注解所在的类生成一个对应的工具类,在其中提供 bind 和 unbind 方法,在这两个方法中来绑定 View 以及解除绑定。

我们现在新建个工程命名为:CopycatKnife。

创建注解类

先创建一个 Java Library 的子模块专门用于存放注解类。我这里将它命名为 annotaions。
毕竟是为了学习 APT,这里就只完成 ButterKnife 的一个 BindView 功能就好了,我们先来定义一个 BindView 注解。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
    int value();
}

因为 BindView 注解是用在属性上的,所以 Target 设置为 Field,然后我们只需要在编译期获取注解并生成代码,所以指定 Retention 为 SOURCE。
BindView 是需要一个 View 的 id 作为参数的,所以这里提供一个返回类型为 int 的方法接收这个参数,这也是注解参数的定义方式,同样可以定义多个参数。

AbstractProcessor

注解处理器需要单独创建一个 Java Library 子模块来存放,我们创建一个名为 complier 的子模块。然后创建一个 BindViewProcessor 类实现 AbstractProcessor 抽象类。这就是我们的注解处理器。
AbstractProcessor 就是 APT 的核心类了,每个 AbstractProcessor 的子类都是一个注解处理器,我们通过继承并实现这个类来完成注解处理器的功能。
该类主要有一下几个方法需要我们实现。

getSupportedAnnotationTypes()

这个方法将会返回一个该注解处理器支持的注解类型。我们目前只支持 BindView 注解,那可以这么写:

override fun getSupportedAnnotationTypes(): Set<String> {
    //返回该处理器可以处理的注解集合
  return setOf(BindView::class.java.canonicalName)
}

我们返回了一个只包含 BindView 注解的 Set 集合。

process

这个方法是关键,真正处理解析注解元素并生成 Java 代码的就是这个方法。
我们看下这个方法的签名:

public abstract boolean process(Set<? extends TypeElement> annotations,
                                RoundEnvironment roundEnv);

第一个参数 annotations 就是当前注解处理器支持的注解集合,第二个 roundEnv 表示当前的 APT 环境,其中提供了很多API,可以通过它获取到注解元素的一些信息。其中最重要的就是 getElementsAnnotatedWith 方法,通过它可以获取到所有使用了该注解的元素。

//获取所有使用了 BindView 注解的元素
val bindViewElementSet = roundEnv.getElementsAnnotatedWith(BindView::class.java)

此时已经获取到了所有使用 BindView 注解的元素。
CopycatKnife 的功能是通过注解直接初始化 View,我这里的方案跟 ButterKnife 一样,都是根据每个使用了 BindView 元素对应的类生成一个绑定类,例如在 MainActivity.java 中有几个使用了 BindView 注解的元素,那么我们就生成一个名为 MainActivity_Binding.java 的类。
而一个项目可能有多个类里面的 View 使用了 BindView 注解,所以第一步应该根据类对元素进行分组。

private fun groupingElementWithType(eleSet: Set<Element>): Map<TypeElement, ArrayList<Element>> {
    val groupedElement = HashMap<TypeElement, ArrayList<Element>>()
    for (item in eleSet) {
        checkAnnotationLegal(item)
        val enclosingElement = item.enclosingElement as TypeElement
        if (groupedElement.keys.contains(enclosingElement)) {
            groupedElement[enclosingElement]!!.add(item)
        } else {
            val list = ArrayList<Element>()
            list += item
            groupedElement[enclosingElement] = list
        }
    }
    return groupedElement
}

在分组前,为了保证元素的合法性,我们先对其进行校验,防止出现错误:

private fun checkAnnotationLegal(ele: Element) {
    if (ele.kind != ElementKind.FIELD) {
        throw RuntimeException("@BindView must in filed! $ele kind is ${ele.kind}")
    }
    val modifier = ele.modifiers
  if (modifier.contains(Modifier.FINAL)) {
        throw RuntimeException("@BindView filed can not be final!")
    }
    if (modifier.contains(Modifier.PRIVATE)) {
        throw RuntimeException("@BindView filed can not be private")
    }
}

校验完成后我们对其进行下一步的操作。
此时需要做的就是解析元素并生成类和方法了,生成 Java 文件这里使用 javapoet 框架:

implementation "com.squareup:javapoet:1.12.1"

关于这个框架的使用方法请看:
https://github.com/square/javapoet

我们现在先创建构造器:

private fun makeConstructor(typeElement: TypeElement): MethodSpec.Builder {
    val typeMirror = typeElement.asType()
    return MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(TypeName.get(typeMirror), "target")
}

如果使用 BindView 注解的元素在 MainActivity 中的话,我们通过上述代码将会生成如下的一个空的构造器:

public MainActivity_Binding(MainActivity target) {}

下面就是实现这个构造器了,也就是在其中对使用了 BindView 注解的元素进行赋值:

for (itemView in elements) {
    bindMethodBuilder.addStatement("target.${itemView} = " +
            "target.findViewById(${itemView.getAnnotation(BindView::class.java).value})")
}

上面会生成类似下面的 Java 代码如下:

target.tvMain01 = target.findViewById(2131165359);

到了这一步主要功能就完成了, 元素已经全部被初始化了。
下面我们再来生成类并把这个方法加进去:

val typeBuilder = TypeSpec.classBuilder("${typeEle.simpleName}_Binding")
        .addModifiers(Modifier.PUBLIC)
typeBuilder.addMethod(constructor)
//生成一个 Java 类文件
val file = JavaFile.builder(getPackageName(classItem), typeBuilder.build())
        .build()
file.writeTo(this.processingEnv.filer)

这样,一个具备绑定及解除绑定元素的完整的类就输出完成了。

创建公开 API 及辅助工具

现在我们还差一个步骤既可完成,我们知道,在使用 BindView 注解时不单单是在元素上使用注解,还需要在 Activity#onCreate 方法中调用 bind 方法绑定才行,然后在 bind 方法中创建我们刚刚通过 APT 生成的 ViewBinding 类,那么我们现在就需要提供一个 bind 方法以及在其中通过反射创建 ViewBinding 类。
所以我们还需要再创建一个 Java Library,然后创建一个 CopycatKnife 类,里面提供一个 bind 方法。

fun bind(activity: Activity) {
    val targetClass = activity::class.java
  val constructor = findBindingConstructorForClass(targetClass)
    constructor?.newInstance(activity)
}

private fun findBindingConstructorForClass(cls: Class<*>?): Constructor<*>? {
    if (cls == null) return null
 var bindingConstructor: Constructor<*>? = null
 val clsName = cls.name
  try {
        val bindingClass = cls.classLoader!!.loadClass(clsName + "_ViewBinding")
        bindingConstructor = bindingClass.getConstructor(cls)
    } catch (e: ClassNotFoundException) {
        bindingConstructor = findBindingConstructorForClass(cls.superclass)
    } catch (e: NoSuchMethodException) {
        throw RuntimeException("Unable to find binding constructor for $clsName", e)
    }
    return bindingConstructor
}

反正这个也不属于 APT 范畴,就不详细介绍了,而且也是仿照(抄袭)ButterKnife 的。

通过上面几个步骤,我们的 CopycatKnife 就基本完成了。

创建配置文件

现在再来个简单的配置就行了,我们需要声明一下各个创建的 APT,现在回到 complier 模块中,先在 main 目录下创建配置文件目录,路径为:

main\resources\META-INF\services

然后在其中创建一个 名为 javax.annotation.processing.Processor 的配置文件,将刚刚创建的 BindViewProcessor 全限定名称加入其中:

com.zhangke.complier.BindViewProcessor

好了,配置代码就这么多。

使用

现在已经全部搞定了,使用就很简单了,配置好依赖后按照 ButterKnife 的使用方式一样使用即可。
全部代码包括使用案例已经放在了我的 Github 上,点击下面的连接查看:
https://github.com/0xZhangKe/CopycatKnife

好了,简陋山寨抄袭版的 ButterKnife 就完成啦,关于 APT 技术的使用就介绍到这了,欢迎关注我的公众号,还有更多干货。

如果觉得还不错的话,欢迎关注我的个人公众号:zhangke_blog

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

推荐阅读更多精彩内容