APT 的基本使用

背景

APT 全称 Annotation Processing Tool,主要是用来处理注解的。我们可以使用注解标记代码中的某些逻辑,比如类,方法,域等,然后在编译的javac过程中就会调用到注解处理器进行注解的处理。一般可以进行辅助代码生成或者是一些问题检测。

最近在项目中使用到了MultiType(https://github.com/drakeet/MultiType) 这个框架,实现一个拥有多种类型item的列表还是挺好用的,不过就是需要写比较多的binder类。

github的demo可以看到每次添加一个类型的ViewHolder,都需要创建多一个Binder类:


class PostViewBinder : ItemViewBinder<Post, PostViewBinder.ViewHolder>() {

  override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {

    return ViewHolder(inflater.inflate(R.layout.item_post, parent, false))

  }

  override fun onBindViewHolder(holder: ViewHolder, item: Post) {

    holder.setData(item)

  }

  class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    private val cover: ImageView = itemView.findViewById(R.id.cover)

    private val title: TextView = itemView.findViewById(R.id.title)

    fun setData(post: Post) {

      cover.setImageResource(post.coverResId)

      title.text = post.title

    }

  }

}

这个Binder类主要用来生厂ViewHolder的,功能比较简单并且单一,所以这里我们可以使用APT来进行代码生成,做一个代码生成demo。

实现

接下来就开始实现这个工厂类的生成demo,顺便学习一下APT的基本使用。

api工程

创建一个java工程,主要做最顶层的依赖,以及一些通用的接口。

api工程的build.gradle


apply plugin: 'java-library'

dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    api('me.drakeet.multitype:multitype:3.4.0')

}

sourceCompatibility = "7"

targetCompatibility = "7"

这里依赖了mutitype

另外还有一个IViewHolder的接口,主要每一个ViewHolder都需要实现该接口


public interface IViewHolder<T> {

    void onBindData(T t);

}

annotation工程

主要是声明注解类,也是一个java library

build.gradle


apply plugin: 'java-library'

dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

}

sourceCompatibility = "1.8"

targetCompatibility = "1.8"

创建一个ViewHolderBinder的注解接口,用于返回ViewHolder对应的xml


@Target(ElementType.TYPE)

@Retention(RetentionPolicy.SOURCE)

//@Documented

//@Inherited

public @interface ViewHolderBinder {

    int xml() default 0;

}

这个注解接口上面还有几个注解声明:

1、Target

代表着我们的注解可以使用在哪些地方,主要取值有以下:


public enum ElementType {

    TYPE,  // 对类,接口

    FIELD,   // 域

    METHOD,  // 方法

    PARAMETER, // 参数声明

    CONSTRUCTOR, // 构造方法

    LOCAL_VARIABLE,  // 局部变量

    ANNOTATION_TYPE,    // 注解类型

    PACKAGE,    // 包声明

    TYPE_PARAMETER, // 可以使用在type的声明

    TYPE_USE; // 可以使用在所有Type的使用地方,比如泛型

    private ElementType() {

    }

}

2、Retention

主要代表这个注解的生命周期限制,主要有以下选择


public enum RetentionPolicy {

    SOURCE,  // 存在源码中,编译为class之后就没有了

    CLASS,    // 存在class中,运行的时候就没有了

    RUNTIME;  // 直到运行的时候一直都存在

    private RetentionPolicy() {

    }

}

3、Documented

表明这个注解应该被javadoc工具记录,默认情况下javadoc是不包括注解的,但如果声明了的话则会包含在其中。

4、Inherited

声明被注解标注的类具有自动继承属性。也就是说我们使用Inherited来声明我们定义的ViewHolderBinder注解,然后使用ViewHolderBinder注解来修饰一个类A,而类A有一个子类B,那么B就会自己继承我们的ViewHolderBinder的修饰,不需要手动处理。

这里我们主要把注解用于ViewHolder上,所以Target使用的是Type,而只是在Javac的编译过程中有效,也就是源码到class的过程,所以就指定为Source即可。

compiler工程

compiler工程是主要作用的工程,也是一个java工程

build.gralde


apply plugin: 'java-library'

dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    api 'com.google.auto.service:auto-service:1.0-rc6'

    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

    api 'com.squareup:javapoet:1.7.0'

    api project(":annotation")

}

sourceCompatibility = "1.7"

targetCompatibility = "1.7"

这里我们需要依赖annotation工程,需要处理其声明的注解类;另外还依赖了auto-service,这个可以帮我们自动注册Processor(注意gradle版本>=5.0 build plugin >= 3.4 的时候就需要annotationProcessor auto-service, 不然的话就跑不起来,我也是踩了个大坑);另外还有javapoet工具,辅助我们生产java类。

在这个工程下创建一个AnnotationProcessor类


@AutoService(Processor.class)

public class AnnotationProcessor extends AbstractProcessor

注意要添加上@AutoService的注解。

如果不添加该注解的话则需要我们再当前工程目录src/main下创建resources\META-INF\services\目录,然后在该目录下创建文件javax.annotation.processing.Processor,在文件中写上我们的AnnotationProcessor的类路径:com.example.compiler.AnnotationProcessor,这样子才算注册成功。

接下来看一下我们AnnotationProcessor的主要逻辑:


    @Override

    public synchronized void init(ProcessingEnvironment processingEnvironment) {

        super.init(processingEnvironment);

        mMessager = processingEnvironment.getMessager();

        mMessager.printMessage(Diagnostic.Kind.NOTE, "init");

    }

init 方法,processingEnvironment是类似context一样的东西,我们需要的“工具”都在其中,这里我只是本地存了一个Messager对象,主要用于打印日志。


    @Override

    public Set<String> getSupportedAnnotationTypes() {

        return Collections.singleton(ViewHolderBinder.class.getCanonicalName());

    }

这个方法主要返回一个能够处理的注解列表,只有当前工程中有我们AnnotationProcessor支持处理的注解修饰,我们的processor才会跑起来。


    @Override

    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        mMessager.printMessage(Diagnostic.Kind.NOTE, "process");

        try {

            createFactoryAndHelper(roundEnvironment);

        } catch (IOException e) {

            e.printStackTrace();

        }

        return false;

    }

如果当前工程中有我们定义的AnnotationProcessor能够处理的注解声明的话,那么process方法就会被调用。综合上面的需求,我们需要使用javapoet在这里为每一个ViewHolder生成一个Factory类以及一个MultiTypeHelper类。

我们先设定要生成的类的样子。
Factory类:


public class Bean1ViewHolderFactory extends ItemViewBinder<Bean1, Bean1ViewHolder> {

  @Override

  public Bean1ViewHolder onCreateViewHolder(LayoutInflater inflater, ViewGroup parent) {

    final View view = inflater.inflate(2131296286, parent, false);

    return new Bean1ViewHolder(view);

  }

  @Override

  public void onBindViewHolder(final Bean1ViewHolder vh, final Bean1 data) {

    vh.onBindData(data);

  }

}

只有两个方法,第一个方法主要是把注解上的xml的id对应的布局给inflate出来,并且作为ViewHolder的构造器创建ViewHolder;第二个方法则是直接把数据丢给了ViewHolder做处理了。

Helper类:


public class MyMultiTypeHelper {

  public static void bindVHFactory(MultiTypeAdapter adapter) {

    adapter.register(Bean1.class, new Bean1ViewHolderFactory());

    adapter.register(Bean2.class, new Bean2ViewHolderFactory());

  }

}

只有一个静态方法,通过传入一个adapter进行注册操作,不用我们每次添加一种类型就需要register一次。

我们看看具体的生成逻辑:


    private void createFactoryAndHelper(RoundEnvironment roundEnvironment) throws IOException {

        Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(ViewHolderBinder.class);

        if (set != null) {

            ArrayList<BinderInfo> binderList = new ArrayList<>();

            for (Element element : set) {

                if (element.getKind() == ElementKind.CLASS) {

                    BinderInfo binderInfo = createFactory((TypeElement)element);

                    binderList.add(binderInfo);

                }

            }

            crateMyMultiTypeHelper(binderList);

        }

    }

先获取到ViewHolderBinder注解声明的类,然后遍历进行ViewHolderFactory的生成,然后每次生成都会返回一个BinderInfo对象,最后通过这个binderInfo的list进行MyMultiTypeHelper对象的生成。


class BinderInfo {

        TypeName dataClass;

        TypeName viewHolderClass;

}

BinderInfo主要存着一个Bean到ViewHolder的映射。


    private static final ClassName inflaterClass = ClassName.get("android.view", "LayoutInflater");

    private static final ClassName viewClass = ClassName.get("android.view", "View");

    private static final ClassName viewGroupClass = ClassName.get("android.view", "ViewGroup");

    private static final ClassName multiTypeAdapter = ClassName.get("me.drakeet.multitype", "MultiTypeAdapter");

    private static final ClassName itemViewBinder = ClassName.get("me.drakeet.multitype", "ItemViewBinder");

我们先对几个后面会使用到的类进行定义(javapoet的具体使用方法可以查看github的介绍 https://github.com/square/javapoet


/**

    * 创建具体工厂类

    * @param element

    */

    private BinderInfo createFactory(TypeElement element) throws IOException {

        // 获取到data ViewHolder 以及xml信息

        TypeName contentDataClassName = getContentClassName(element);

        TypeName viewHolderClassName = TypeName.get(element.asType());

        int xmlType = element.getAnnotation(ViewHolderBinder.class).xml();

        // 创建onCreateViewHolder方法

        MethodSpec onCreateViewHolderMethod = MethodSpec.methodBuilder("onCreateViewHolder")

                .addModifiers(Modifier.PUBLIC)

                .addAnnotation(Override.class)

                .addParameter(inflaterClass, "inflater")

                .addParameter(viewGroupClass, "parent")

                .returns(TypeName.get(element.asType()))

                .addCode("final $T view = inflater.inflate($L, parent, false);\n", viewClass, xmlType)

                .addCode("return new $T(view);\n", viewHolderClassName)

                .addModifiers()

                .build();

        // 创建onBindViewHolder方法

        MethodSpec onBindViewHolderMethod = MethodSpec.methodBuilder("onBindViewHolder")

                .addModifiers(Modifier.PUBLIC)

                .addAnnotation(Override.class)

                .addParameter(viewHolderClassName,"vh", Modifier.FINAL)

                .addParameter(contentDataClassName, "data", Modifier.FINAL)

                .addCode("vh.onBindData(data);\n")

                .build();

        // 实现的接口对象

        TypeName parentClassType = ParameterizedTypeName.get(itemViewBinder, contentDataClassName, viewHolderClassName);

        // 创建XXFactory类对象

        TypeSpec typeSpec = TypeSpec.classBuilder(element.getSimpleName() + "Factory")

                .addModifiers(Modifier.PUBLIC)

                .superclass(parentClassType)

                .addMethod(onCreateViewHolderMethod)

                .addMethod(onBindViewHolderMethod)

                .build();

        // 写入文件,生成.java文件

        JavaFile javaFile = JavaFile.builder("com.hh.example", typeSpec).build();

        javaFile.writeTo(processingEnv.getFiler());

        // 返回获取到的信息

        BinderInfo binderInfo = new BinderInfo();

        binderInfo.viewHolderClass = viewHolderClassName;

        binderInfo.dataClass = contentDataClassName;

        return binderInfo;

    }

这是生成XXXFactory的逻辑。


private void crateMyMultiTypeHelper(ArrayList<BinderInfo> binderList) throws IOException {

        // 方法体内容

        CodeBlock.Builder codeBlockBuilder = CodeBlock.builder();

        for (BinderInfo info : binderList) {

            codeBlockBuilder.add("adapter.register($T.class, new $TFactory());\n", info.dataClass, info.viewHolderClass);

        }

        // 创建bindVHFactory方法

        MethodSpec getBindersMethod = MethodSpec.methodBuilder("bindVHFactory")

                .addModifiers(Modifier.STATIC, Modifier.PUBLIC)

                .addParameter(multiTypeAdapter, "adapter")

                .returns(void.class)

                .addCode(codeBlockBuilder.build())

                .build();

        // 创建MyMultiTypeHelper 对象

        TypeSpec typeSpec = TypeSpec.classBuilder("MyMultiTypeHelper")

                .addModifiers(Modifier.PUBLIC)

                .addMethod(getBindersMethod)

                .build();

        // 写入文件

        JavaFile javaFile = JavaFile.builder("com.hh.example", typeSpec).build();

        javaFile.writeTo(processingEnv.getFiler());

    }

生成MyMultiTypeHelper类的逻辑。

app工程

最后就是使用的逻辑了。

build.gradle


dependencies {

    api project(':annotation')

    api project(':api')

    annotationProcessor project(':compiler')

}

主要是dependcies部分,需要依赖annotation和api,然后还需要注册annotation处理module compiler。

代码的话声明了两个Bean类,Bean1.java, Bean2.java

然后对应声明了两个ViewHolder类,Bean1ViewHolder.java Bean2ViewHolder.java. 都差不多,贴一下其中一个的代码:


@ViewHolderBinder(xml = R.layout.item_bean1)

public class Bean1ViewHolder extends RecyclerView.ViewHolder implements IViewHolder<Bean1> {

    public Bean1ViewHolder(View itemView) {

        super(itemView);

    }

    @Override

    public void onBindData(Bean1 bean) {

    }

}

指定了布局为item_bean1.

在RecyclerView的使用逻辑为:


        MultiTypeAdapter adapter = new MultiTypeAdapter();

        MyMultiTypeHelper.bindVHFactory(adapter);

        adapter.setItems(createTestItem());

        RecyclerView mRecyclerView = findViewById(R.id.recycler_view);

        mRecyclerView.setAdapter(adapter);

        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

这样子就ok了。以后我们添加新的item只需要添加布局,Bean类,ViewHolder类即可,其他的逻辑都可以不用动了。
编译后我们可以在工程的app\build\generated\source\apt\debug\com\hh\example 路径下看到生成的类了。

总结

通过这样的一个demo学习到了APT工具的使用基本流程,不过还有一些其他的问题,比如AutoService原理,AnnotationProcessor注册的原理,javaPoet的生成原理等问题还可以继续学习。

这里附上demo的源码:https://github.com/huanghuanhuanghuan/APTDemo.git

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

推荐阅读更多精彩内容