张可的博客

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

基本概念

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

注解

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

定义一个注解

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

public @interface Test {}

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

元注解

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

Retention

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

Target

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

@Target(ElementType.TYPE_PARAMETER)
public @interface Test {}

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

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

做 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 技术的使用就介绍到这了,欢迎关注我的公众号,还有更多干货。