上一期我们已经把butterknife-annotations中的注解变量都已经定义好了,分别为BindView、OnClick与Keep。

如果你是第一次进入本系列文章,强烈推荐跳到文章末尾查看上篇文章,要不然你可能会有点云里雾里。

如果在代码中引用的话,它将与开源库ButterKnife的操作类似。

class MainActivity : AppCompatActivity() {@BindView(R.id.public_service, R.string.public_service)lateinit var sName: TextView@BindView(R.id.personal_wx, R.string.personal_wx)lateinit var sPhone: TextViewoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)Butterknife.bind(this)}@OnClick(R.id.public_service)fun nameClick(view: View) {Toast.makeText(this, getString(R.string.public_service_click_toast), Toast.LENGTH_LONG).show()}@OnClick(R.id.personal_wx)fun phoneClick(view: View) {Toast.makeText(this, getString(R.string.personal_wx_click_toast), Toast.LENGTH_LONG).show()}
}

使用@BindView来绑定我的View,使用@OnClick来绑定View的点击事件。使用Butterknife.bind来绑定该Class,主要是用来实例化自动生成的类。(该部分下篇文章将提及)

我们自己定义的绑定注解库已经完成了1/3,接下来我们将实现它的代码自动生成部分。这时就到了上期提到的第二个Module:butterknife-compiler。

NameUtils是一些常量的管理工具类。

final class NameUtils {static String getAutoGeneratorTypeName(String typeName) {return typeName + ConstantUtils.BINDING_BUTTERKNIFE_SUFFIX;}static class Package{static final String ANDROID_VIEW = "android.view";}static class Class {static final String CLASS_VIEW = "View";static final String CLASS_ON_CLICK_LISTENER = "OnClickListener";}static class Method{static final String BIND_VIEW = "bindView";static final String SET_ON_CLICK_LISTENER = "setOnClickListener";static final String ON_CLICK = "onClick";}static class Variable{static final String ANDROID_ACTIVITY = "activity";}
}

NameUitls包含了自动生成的类名称,包名,方法名,变量名。总之就是为了代码更健全,方便管理。

第二个类Processor是今天的重中之重。也是注解库代码自动生成的核心部分。由于注解的自动生成代码都是在注解进程中进行,所以这里它继承于AbstractProcessor,其中主要有三个方法需要实现。

  1. init:初始化必要的数据
  2. getSupportedAnnotationTypes:所支持的注解
  3. process:解析注解,编写自动生成代码

init

从简单到容易,先是init方法,我们直接看代码

    @Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);mFiler = processingEnv.getFiler();mMessager = processingEnv.getMessager();mElementUtils = processingEnv.getElementUtils();}

方法参数processingEnv为我们提供注解处理所需的环境状态。我们通过getFiler()、getMessager()与getElementUthis()方法,分别获取创建源代码的Filer、消息发送器Messager(主要用于向外界发送错误信息)与解析注解元素所需的通用方法。

例如:当我们已经构建好了需要自动生成的类,这时我们就可以使用Filter来将代码写入到java文件中,如遇错误使用Messager将错误信息发送出去。

//写入java文
try {JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler)
} catch (IOException e) {mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement);
}

代码中的JavaFile与typeBuilder都是JavaPoet中的类。JavaPote主要提供Java API来帮助生成.java资源文件。

getSupportedAnnotationTypes

    @Overridepublic Set<String> getSupportedAnnotationTypes() {return new TreeSet<>(Arrays.asList(BindView.class.getCanonicalName(),OnClick.class.getCanonicalName(),Keep.class.getCanonicalName()));}

看方法名就知道了,包含所支持的注解,将其通过set集合来返回。这里将我们上一期自定义的注解添加到set集合中即可。

process

到了本篇文章的核心,process用来生成与注解相匹配的方法代码。通过解析Class中定义的注解,生成与注解相关联的类。

    @Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {........return true;}

提供了两个参数:annotations与roundEnv,分别代表需要处理的注解,这里就代表我们自定义的注解;注解处理器所需的环境,帮助进行解析注解。

在开始解析注解之前,我们应该先过滤我们所不需要的注解。回头看getSupportedAnnotationTypes方法,我们只支持BindView、OnClick与Keep这三个注解。为了解析出相匹配的注解,我们将这个逻辑单独抽离出来,交由getTypeElementsByAnnotationType来管理。

    private Set<TypeElement> getTypeElementsByAnnotationType(Set<? extends TypeElement> annotations, Set<? extends Element> elements) {Set<TypeElement> result = new HashSet<>();//遍历包含的 package class methodfor (Element element : elements) {//匹配 class or interfaceif (element instanceof TypeElement) {boolean found = false;//遍历class中包含的 filed method constructorsfor (Element subElement : element.getEnclosedElements()) {//遍历element中包含的注释for (AnnotationMirror annotationMirror : subElement.getAnnotationMirrors()) {for (TypeElement annotation : annotations) {//匹配注释if (annotationMirror.getAnnotationType().asElement().equals(annotation)) {result.add((TypeElement) element);found = true;break;}}if (found) break;}if (found) break;}}}return result;}

首先理解Element是什么?Element代表程序中的包名、类、方法,这也是注解所支持的作用类型。然后再回到代码部分,已经给出详细代码注释。
该方法的作用就是获取到有我们自定义注解的class。这里介绍两个主要的方法

  1. getEnclosedElements():获元素中的闭包的注解元素,在我们的实例中元素为MainActivity(TypeElement,Type代表Class),而闭包的注解元素则为sName、sPhone、nameClick、phoneClick与onCreate。在这里简单的理解就是获取有注解的字段名、方法名
  2. getAnnotationMirrors():获取上述闭包元素的所有注解。这里分别为sName与sPhone上的@BindeView、nameClick与phoneClick上的@OnClick、onCreate上的@Override。

所以通过该方法最终返回的就是MainActivity,它将被转化为TypeElement类型返回,然后将由processing来处理。

我们再回到process方法中。通过getTypeElementsByAnnotationType()方法我们已经获取到了我们使用了自定义注解的TypeElement(MainActivity)。

//获取与annotation相匹配的TypeElement,即有注释声明的class
Set<TypeElement> elements = getTypeElementsByAnnotationType(annotations, roundEnv.getRootElements());

下面我们再获取构建类所需的相关信息。

//包名
String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//类名
String typeName = typeElement.getSimpleName().toString();
//全称类名
ClassName className = ClassName.get(packageName, typeName);
//自动生成类全称名
ClassName autoGenerationClassName = ClassName.get(packageName,NameUtils.getAutoGeneratorTypeName(typeName));//构建自动生成的类
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName).addModifiers(Modifier.PUBLIC).addAnnotation(Keep.class);

注释已经划分清楚了,可以分为四个步骤

  1. 获取对应typeElement的包名(这里获取的是com.idisfkj.androidapianalysis)
  2. 获取typeElement的SimpleName(这里为MainActivity字符串)
  3. 根据上述获取的包名与SimpleName来构建一个ClassName,为了后续声明方法的参数类型(这里为MainActivity类,注意是MainActivity类型)
  4. 构建需要自动生成的ClassName,这里使用NameUtils.getAutoGeneratorTypeName进行了统一命名(这里自动生成的类名为MainActivity$Binding,都以原始类名后面加$Binding)

所有信息准备完毕后,然后开始定义自动生成的类。这里通过使用TypeSpec.Builder来构建。它是JavaPoet中的类。

JavaPoet

由于直接使用JavaFileObject生成.java资源文件是非常麻烦的,所以推荐使用JavaPoet。JavaPoet是一个开源库,主要用来帮助方便快捷的生成.java的资源文件。想要全面了解的可以查看Github链接。为了帮助快速读懂该文章,这里对其中几个主要方法进行介绍。当然在使用前还需在butterknife-compiler中的builder.gradle添加依赖:

dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation project(':butterknife-annotations')implementation 'com.squareup:javapoet:1.11.1'
}

同时也将上一期我们自定义的注解Module引入。

  1. TypeSpec.Builder: 定义一个类
  2. addModifiers: 定义private、public与protected类型
  3. addAnnotation: 对Element元素添加注解。例如:@Keep
  4. TypeSpec.Builder -> addMethod: 添加方法
  5. MethodSpec -> addParameter: 为方法添加参数类型与参数名
  6. MethodSpec -> addStatement: 在方法中添加代码块。而其中的一些动态类型会使用占位符替代。例如:addStatement("$N($N)", "bindView", "activity"),它将会生成bindView(activity)。占位符:$N -> name, $T -> type(ClassName), $L -> literals

有了上面的理解我们再来看下面的生成代码:

//构建自动生成的类
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName).addModifiers(Modifier.PUBLIC).addAnnotation(Keep.class);//添加构造方法
typeBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY).addStatement("$N($N)",NameUtils.Method.BIND_VIEW,NameUtils.Variable.ANDROID_ACTIVITY).addStatement("$N($N)",NameUtils.Method.SET_ON_CLICK_LISTENER,NameUtils.Variable.ANDROID_ACTIVITY).build());

首先通过TypeSpec.Builder构建一个类,类名为autoGenerationClassName(MainActivity$Binding),类的访问级别为public,由于为了防止混淆使用了我们自定义的@Keep注解。

然后再来添加类的构造方法,使用addMethod、addModifiers、addParameter与addStatement分别构建构造方法名、方法访问级别、方法参数与方法中执行的代码块。所以上面的代码最终将会自动生成如下代码:

@Keep
public class MainActivity$Binding {public MainActivity$Binding(MainActivity activity) {bindView(activity);setOnClickListener(activity);}
}  

在自动生成类的构造方法中调用了我们想要的bindView与setOnClickListener方法。所以接下来我们要实现的就是这两个方法的构建。

bindView

//添加bindView成员方法
MethodSpec.Builder bindViewBuilder = MethodSpec.methodBuilder(NameUtils.Method.BIND_VIEW).addModifiers(Modifier.PRIVATE).returns(TypeName.VOID).addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY);//添加方法内容
for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {BindView bindView = variableElement.getAnnotation(BindView.class);if (bindView != null) {bindViewBuilder.addStatement("$N.$N=($T)$N.findViewById($L)",NameUtils.Variable.ANDROID_ACTIVITY,variableElement.getSimpleName(),variableElement,NameUtils.Variable.ANDROID_ACTIVITY,bindView.value()[0]).addStatement("$N.$N.setText($N.getString($L))",NameUtils.Variable.ANDROID_ACTIVITY,variableElement.getSimpleName(),NameUtils.Variable.ANDROID_ACTIVITY,bindView.value()[1]);}
}typeBuilder.addMethod(bindViewBuilder.build());

使用MethodSpec.Builder来创建bindView方法,其它的都与构造方法类似。使用returns为方法返回void类型。然后再遍历MainActivity中的注解,找到与我们定义的BindView相匹配的字段。最后分别向bindView方法中添加findViewById与setText代码块,同时将定义的方法添加到typeBuilder中。所以执行完上面代码后在MainActivity$Binding中展示如下:

  private void bindView(MainActivity activity) {activity.sName=(TextView)activity.findViewById(2131165265);activity.sName.setText(activity.getString(2131427362));activity.sPhone=(TextView)activity.findViewById(2131165262);activity.sPhone.setText(activity.getString(2131427360));}

实现了我们最初的View的绑定与TextView的默认值设置。

setOnClickListener

//添加setOnClickListener成员方法
MethodSpec.Builder setOnClickListenerBuilder = MethodSpec.methodBuilder(NameUtils.Method.SET_ON_CLICK_LISTENER).addModifiers(Modifier.PRIVATE).returns(TypeName.VOID).addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY, Modifier.FINAL);//添加方法内容
ClassName viewClassName = ClassName.get(NameUtils.Package.ANDROID_VIEW, NameUtils.Class.CLASS_VIEW);
ClassName onClickListenerClassName = ClassName.get(NameUtils.Package.ANDROID_VIEW, NameUtils.Class.CLASS_VIEW, NameUtils.Class.CLASS_ON_CLICK_LISTENER);for (ExecutableElement executableElement : ElementFilter.methodsIn(typeElement.getEnclosedElements())) {OnClick onClick = executableElement.getAnnotation(OnClick.class);if (onClick != null) {//构建匿名classTypeSpec typeSpec = TypeSpec.anonymousClassBuilder("").addSuperinterface(onClickListenerClassName).addMethod(MethodSpec.methodBuilder(NameUtils.Method.ON_CLICK).addModifiers(Modifier.PUBLIC).addParameter(viewClassName, NameUtils.Class.CLASS_VIEW).returns(TypeName.VOID).addStatement("$N.$N($N)",NameUtils.Variable.ANDROID_ACTIVITY,executableElement.getSimpleName(),NameUtils.Class.CLASS_VIEW).build()).build();setOnClickListenerBuilder.addStatement("$N.findViewById($L).setOnClickListener($L)",NameUtils.Variable.ANDROID_ACTIVITY,onClick.value(),typeSpec);}
}typeBuilder.addMethod(setOnClickListenerBuilder.build());

与bindView方法不同的是,由于使用到了匿名类OnClickListener与类View,所以我们这里也要定义他们的ClassName,然后使用TypeSpec来生成匿名类。生成之后再添加到setOnClickListener方法中。最后再将setOnClickListener方法添加到MainActivity$Binding中。所以最终展示如下:

  private void setOnClickListener(final MainActivity activity) {activity.findViewById(2131165265).setOnClickListener(new View.OnClickListener() {public void onClick(View View) {activity.nameClick(View);}});activity.findViewById(2131165262).setOnClickListener(new View.OnClickListener() {public void onClick(View View) {activity.phoneClick(View);}});}

我们的MainActivity$Binding类就已经定义完成,最后再写入到java文件中

//写入java文件
try {JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler);
} catch (IOException e) {mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement);
}

services

在butterknife-compiler中,我们还需创建一个特定的目录:
butterknife-compiler/src/main/resources/META-INF/services

在services目录中,我们还需创建一个文件:javax.annotation.processing.Processor,该文件是用来告诉编译器,当它在编译代码的过程中正处于注解处理中时,会告诉注解处理器来自动生成哪些类。

所以我们在文件中将添加我们自定义的Processor路径

com.idisfkj.butterknife.compiler.Processor

这样注解器就会调用该指定的Processor。到这里整个butterknife-compiler就完成了,现在我们可以Make Project一下工程,完成之后就可以全局搜索到MainActivity$Binding文件了。或者在如下路径中查看:

/app/build/generated/source/kapt/debug/com/idisfkj/androidapianalysis/MainActivity$Binding.java

文章中的代码都可以在Github中获取到。使用时请将分支切换到feat_annotation_processing

相关文章

自定义Android注解Part1:注解变量

自定义Android注解Part3:绑定

关注

公众号:怪谈时间到了

自定义Android注解Part2:代码自动生成相关推荐

  1. 自定义Android注解Part3:绑定

    上一节我们已经将自动生成注解代码部分介绍完毕,今天这篇文章是自定义Android注解系列的最后一篇文章.希望大家这一路走来有所收获. 经过前面的了解,我们三大部分:butterknife-annota ...

  2. java代码自动生成的插件_如何使用插件生成自定义Java 8代码

    java代码自动生成的插件 大多数程序员讨厌的一件事就是编写样板代码. 无休止的时间花费在设置实体类和配置数据库连接上. 为了避免这种情况,您可以让Speedment Open Source之类的程序 ...

  3. k8s自定义controller三部曲之二:自动生成代码

    本文是<k8s自定义controller三部曲>的第二篇,上一篇我们在k8s环境注册了API对象Student,此时如果创建Student对象就会在etcd保存该对象信息: 三部曲所有文章 ...

  4. eladmin代码自动生成_如何让 Mybatis 自动生成代码

    1 创建代码生成器 1.1 创建Maven项目 1.2 配置 generator.xml 1.3 配置 pom.xml 1.4 使用及测试 2 XML 配置详解 2.1 优先 2.2 官网没有的 &l ...

  5. eladmin代码自动生成_如何让Mybatis自动生成代码

    点击上方"Java知音",选择"置顶公众号" 技术文章第一时间送达! 作者:阿进的写字台 cnblogs.com/homejim/p/9782403.html ...

  6. Android Studio的res自动生成的文件出错了_莫韵乐与bug的奇妙冒险

    Android Studio的res自动生成的文件出错了_莫韵乐与bug的奇妙冒险 无论学什么编程语言都要跟奇怪的bug过不去 奇怪的bug出现了 今天满怀欣喜地进行安卓开发学习,创建好一个新的项目之 ...

  7. 用java写ods系统_基于数据库的代码自动生成工具,生成JavaBean、生成数据库文档、生成前后端代码等(TableGo v7.0.0版)...

    TableGo是基于数据库的代码自动生成工具,低代码编程技术的实现,可以零代码自动生成SpringBoot项目工程.生成JavaBean.生成前后端分离的CRUD代码.生成MyBaits的Mapper ...

  8. 亚马逊开源模型设计神器:AutoGluon,三行代码自动生成SOTA模型!

    点击我爱计算机视觉标星,更快获取CVML新技术 本文转载自新智元. 亚马逊最近宣布推出AutoGluon,这是一个新的开源库,开发人员可以使用该库构建包含图像.文本或表格数据集的机器学习应用程序.使用 ...

  9. java生成iso9660工具_基于数据库的代码自动生成工具,生成JavaBean、生成数据库文档、生成前后端代码等(TableGo v7.0.0版)...

    TableGo_20210212 v7.0.0 正式版发布,此次版本更新如下: 1.新增对DB2数据库的支持 2.新增按字段生成文件,支持把字段.JSON.XML数据转换成任何代码 3.新增大量新的自 ...

最新文章

  1. Step by Step WebMatrix网站开发之二:使用WebMatrix(1)
  2. [论文笔记] A model for correlated failures in N-version programming(IIE Trans, 2004)
  3. 【量化投资】策略九(聚宽)
  4. 二、window下django安装及第一个应用
  5. Linux的make 命令出现:make:*** No targets specified and no makefile found.Stop
  6. Mysql 中如何创建触发器
  7. MySQL性能指标及计算方法
  8. 作者:张金芳(1970-),男,中国科学院软件研究所副研究员
  9. c语言中打印ipv6地址,c-来自任何有效地址的ipv4和ipv6
  10. SSE图像算法优化系列二十一:基于DCT变换图像去噪算法的进一步优化(100W像素30ms)。...
  11. linux buffer/cache手动释放
  12. VC++ 非托管代码 托管代码
  13. 2021-2025年中国滴眼器行业市场供需与战略研究报告
  14. Bom及Bom对象的详细介绍
  15. 几个文件vtp格式转stl格式
  16. IT大败局----第七章 企业并购的陷阱
  17. B.DongDong认亲戚
  18. 马克思主义基本原理概述
  19. TortoiseSVN简明教程
  20. js去掉前后空格的函数_Node.js十年,你大爷还是你大爷

热门文章

  1. php mysql study_phpStudy 升级 MySQL5.7
  2. 机器学习算法python实现
  3. 无人机数车--Drone-based Object Counting by Spatially Regularized Regional Proposal Network
  4. urbansound8k下载地址
  5. xcode 编译静态库所要注意
  6. c语言装饰,C++设计模式之装饰模式(Decorator)
  7. 软件测试中7个令人匪夷所思的真理
  8. selenium + python自动化测试环境搭建
  9. c语言英语笔试题,C语言面试题(国外英语资料).doc
  10. Java泛型之mybatis,基于spring MVC 和 MyBatis 泛型的代码生成模板