背景
编译时注解越来越多的出现在各大开源框架使用中,比如
view
事件
依赖注入
类似这样的库在开发和工作中已经越来越多,它们旨在帮助我们在效率为前提的情况下帮助开发者快速开发,节约时间成本。而它们都使用了编译时注解的思想。
正因为如此火热,所以有必要好好学习其中的实现原理,方便解决因为编译时注解导致的问题,同时可将此技术运用到自己的开源库中
思想
编译时注解框架在编写时有相对固定的格式,分包为例
格式相对固定,但是也可以灵活变动,比如讲api
和annotations
结合在一个moudel
里
moudel中
的依赖关系也非常的固定
processors
依赖包有api
-annotations
app
依赖包有api
-annotations
-processors
其中除了app
是android moudel
以外,其他全部均是java moudel
annotations
注解
在讲解annotations
注解之前,需要对java和android注解有大致的了解,可以参考我之前的博客
先初始一个HelloWordAtion注解标注Target为ElementType.TYPE
修饰类对象
@Retention(RetentionPolicy.CLASS)@Target(ElementType.TYPE)public @interface HelloWordAtion { String value();}复制代码
一般一个注解需要对应一个注解处理器,注解处理器在processors
处理
processors
注解处理器
对应注解的处理器需要继承AbstractProcessor
类,需要复写以下4个方法:
init
init(ProcessingEnvironment processingEnv)
会被注解处理工具调用
process
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
getSupportedAnnotationTypes
etSupportedAnnotationTypes()
这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
@return
注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
getSupportedSourceVersion
指定使用的Java
版本,通常这里返回SourceVersion.latestSupported(),默认返回
SourceVersion.RELEASE_6 `
@return
使用的Java
版本
生成注解处理器
对AbstractProcessor
有了深入的了解,知道核心的初始编译时编写代码的方法及时process
,在process
中我们通过得到传递过来的数据,写入代码,这里先采用打印的方式,简单输出信息,后续会详细讲解如何自己实现 butterknife
功能
public class HelloWordProcessor extends AbstractProcessor { private Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); // Filer是个接口,支持通过注解处理器创建新文件 filer = processingEnv.getFiler(); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) { if (!(element instanceof TypeElement)) { return false; } TypeElement typeElement = (TypeElement) element; String clsNmae = typeElement.getSimpleName().toString(); String msg = typeElement.getAnnotation(HelloWordAtion.class).value(); System.out.println("clsName--->"+clsNmae+" msg->"+msg); } return true; } @Override public SetgetSupportedAnnotationTypes() { Set annotations = new LinkedHashSet<>(); annotations.add(HelloWordAtion.class.getCanonicalName()); return annotations; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }}复制代码
到这一步HelloWordAtion
对应的注解处理器已经编写完成,这里简单的打印了HelloWordAtion
注解的class
和注解指定的value
信息
准备工作完成以后,app
触发调用
@HelloWordAtion("hello")public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}复制代码
这里注解注释的类MainActivity
并且指定value
为hello
,到此准备工作就算完成了,这时如果你直接编译或者运行工程的话,是看不到任何输出信息的,这里还要做的一步操作是指定注解处理器的所在,需要做如下操作:
1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.process.Processors 文件;
4、在 javax.annotation.process.Processors 文件写入注解处理器的全称,包括包路径;
经历了以上步骤以后方可成功运行,但是实在是太复杂了,博主为了配置这一步也是搞了好久,所以这里推荐使用开源框架AutoService
AutoService
直接在Processors
中依赖
compile 'com.google.auto.service:auto-service:1.0-rc2'复制代码
使用
@AutoService(Processor.class)public class HelloWordProcessor extends AbstractProcessor {xxxxxxx}复制代码
到这里运行程序便可以成功看到后台的输出信息
需要切换到右下角的Gradle Console
窗口,如果变异不成功可以clean
工程以后重新运行
得到需要的数据,下一步当然是将数据写入到java class
中,也就是题目所言的编译时注解,如何才能写入,这里需要借助Filer
类
Filer
在AbstractProcessor
的init
方法中初始Filer
private Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); filer = processingEnv.getFiler(); }复制代码
到此我们已经有了写入的类的帮助类,还差代码生成逻辑,这里介绍使用javapoet
javapoet
JavaPoet一个是创建 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,GitHub
上面有非常详细的用法,建议好好阅读相关的使用
processors
依赖:
compile 'com.squareup:javapoet:1.8.0'复制代码
综合上述的技术,仿照javapoet
的第一个Example
生成如下代码
@Override public boolean process(Set annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(HelloWordAtion.class)) { if (!(element instanceof TypeElement)) { return false; } TypeElement typeElement = (TypeElement) element; String clsNmae = typeElement.getSimpleName().toString(); String msg = typeElement.getAnnotation(HelloWordAtion.class).value(); System.out.println("clsName--->"+clsNmae+" msg->"+msg); // 创建main方法 MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, clsNmae+"-"+msg) .build(); // 创建HelloWorld类 TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); try { // 生成 com.wzgiceman.viewinjector.HelloWorld.java JavaFile javaFile = JavaFile.builder("com.wzgiceman.viewinjector", helloWorld) .addFileComment(" This codes are generated automatically. Do not modify!") .build(); // 生成文件 javaFile.writeTo(filer); } catch (IOException e) { e.printStackTrace(); } } return true; }复制代码
这里重点讲解process
方法,也就是写入代码的方法体,我们在javapoet
的Example
基础上将输出信息改为HelloWordAtion
注解获取的信息,到处便完全搞定编译时注解的整个流程,clean
以后运行工程,在如下路径下便可看到自动编译生成的HelloWorld
类
到此简单的编译时注解就搞定了,但是编译时注解的自动写入也会导致代码混乱,可能在多次build
编译过程中出现文件冲突的情况,所以这里需要引入android-apt
android-apt
android-apt
能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留无用的文件,辅助 Android Studio
项目的对应目录中存放注解处理器在编译期间生成的文件
依赖使用:
根目录build.gradle
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'复制代码
app
中
apply plugin: 'com.neenbedankt.android-apt'apt project(':processors')复制代码
这里是apt
替换compile
依赖processors
总结
到此简单的编译时注解就搞定了,但是api
模块还没有涉及,别着急接下来的博客中继续扩展,运用掌握的编译时注解和时下主流的butterknife
框架,实现一套自己的自定义注入框架中会详细讲解api
模块的使用,你会发现原来butterknife
很简单,当然可以自由发散,扩展回到自己的任何开源项目中,替换掉反射提高效率。迫不及待的小伙伴可以去GitHub
下载源码先自行研究。