【Java 基础】Java SPI 二 之 Java APT原理及APT实战 - 一步步教你写ButterKnife
一、定义
Java APT 是 Java 技术设计的一个 APT 架构,
APT(Annotation Processing Tool)即注解处理器,它是一种处理注解的工具,也是javac中的一个工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。
APT可以用来在编译时扫描和处理注解, 它可以用来获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。 在Android中有如ButterKnife、Dagger、EventBus等第三方框架,都采用了APT。
常用的使用方式是这样的:
APT + 注解处理器(AbstractProcess)+ 代码处理(javaPoet)+ 处理器注册(AutoService)
这里我们了解下相关概念:
APT
是一个工具,可以用于检查源代码中的注解,并生成相应的代码。
AbstractProcess:
是 Java 中的一个抽象类,用于定义一个处理程序或者一个抽象的执行环境。在 Java 的编程过程中,经常需要在类的开头或者方法中加入注解,来标记这个方法或者类是抽象的,或者是实现了某个接口。这时候,AbstractProcess 类就可以用来定义注解处理器,用来处理这些注解。
APT框架中的核心类是AbstractProcessor,它定义了如何在代码中处理注解。当APT工具扫描文件时,它会调用实现AbstractProcessor类的一个或多个类的process()方法,以捕获和处理注解。
二、APT工作原理
Java Annotations Processing Tool(APT)是一个预处理器,可以在Java代码编译期间读取注解,并生成相关的代码。
它的工作原理如下:
- Java编译器会将源文件传递给APT进行处理;
- APT会扫描源文件中所有的注解,并找到对应的处理器;
- 处理器会对注解进行处理,并生成新的Java代码文件(或其他文件);
- 生成的Java代码文件被编译成字节码文件;
- 编译器将生成的字节码文件和原始Java代码文件一起打包成jar包或class文件。
APT通过Java标准类库中的javax.annotation.processing包提供注解处理的框架。注解处理器必须实现该包中的特定接口,这些接口定义了APT框架的核心功能。通过实现接口,注解处理器能够直接访问来自编译器的数据,以及用于注解处理的元数据信息。注解处理器总是运行在 Java 编译环境中。
APT的使用可以帮助简化一些重复、冗杂的代码生成工作。
三、APT实战1(运行时注解) - 一步步教你写ButterKnife
使用运行时注解的方式实战简单APT,这个比较简单,就是用反射的方式来实现,
void injectLayout(Context context) { // 1. 获取当前class Class> clazz = context.getClass(); // 2. 根据class获取class上面的注解 InjectContenttLayout annotation = clazz.getAnnotation(InjectContenttLayout.class); // 3. 获取注解中布局文件的id的值 int layoutId = annotation.value(); try { // 4. 获取activity中的setContentView方法 Method method = clazz.getMethod("setContentView", int.class); // 5. 执行setContentView方法,传入layoutId参数 method.invoke(context, layoutId); } catch (Exception e) { } }
四、APT实战2(编译时注解) - 一步步教你写ButterKnife
使用编译时注解的方式实战简单APT,手写ButterKnife框架,我们来写一下布局文件view的注入,比如我们不想写烦人的findViewById方法,直接用个注解来搞定,
其核心思想是java的ioc(inversion of control),也叫di(dependency injection,依赖注入),是一种面向对象编程中的设计模式。下面我们开始
4.1 创建一个项目,如下图
4.2 新建自定义注解
创建一个Java Library Module名称叫 apt-annotation
在这个module中创建自定义注解 @BindView,如下图
4.3 实现APT Compiler处理注解
创建一个Java Library Module名称叫 apt-compiler-processor,并添加注解module依赖
dependencies { implementation project(':apt-annotation')}
这个module的作用主要是用来处理注解,并生成java帮助类文件,拆解步骤为
扫描所有被注解标记的Element,获得注解标记的element
遍历Element,并根据不同的页面进行分类
按规则进行字符串拼接,用于拼接生成帮助类代码
使用JavaFileObject写入文件生成java代码
(输出的文件在build->generated-ap_generated_sources->debug->out->包名目录下)
如下图所示
该module在处理注解时,必须继承AbstractProcessor抽象类,入口类为process(Set extends TypeElement> set, RoundEnvironment roundEnv)
@Override public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnv) { System.out.println("start process"); if (set != null && set.size() != 0) { //1、 扫描所有被注解标记的Element,获得被BindView注解标记的element Set extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class); categories(elements); for (TypeElement typeElement : mToBindMap.keySet()) { // 获取帮助类所有代码 String code = generateCode(typeElement); // 构建要生成的帮助类的类名 String helperClassName = typeElement.getQualifiedName() + "_ButterKnifeTest"; // System.out.println("start process 帮助类的类名= " + helperClassName); // 输出帮助类的java文件, // 在本例中就是MainActivity_ButterKnifeTest.java文件 // 输出的文件在build->generated-ap_generated_sources->debug->out->包名目录下 try { System.out.println("生成帮助类 "); JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement); Writer writer = jfo.openWriter(); writer.write(code); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } } return true; } return false; }
当代码写完后,就需要注册APT(如上图所示)
注册一个APT需要以下步骤:
- 在main 目录下新建 resources 资源文件夹;
- 在 resources文件夹下建立 META-INF/services 目录文件夹;
- 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
- 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
4.4 以上步骤全部完成后, 还需要对外提供API(当然也可以不拆分)
创建一个Android Library Module,名称叫apt-api
,并添加依赖
dependencies { ... api project(':apt-annotation')}
实现很简单,就是通过反射去调用APT生成的帮助类的方法去实现View的自动绑定,部分代码如下:
public void inject(Object target) { String className = target.getClass().getCanonicalName(); String helperName = className + "_ButterKnifeTest"; System.out.println("ButterKnifeTest inject" + helperName); try { IBindHelper helper = (IBindHelper) (Class.forName(helperName).getConstructor().newInstance()); helper.inject(target); } catch (Exception e) { e.printStackTrace(); } }
最后就是使用了,在app module里添加依赖
dependencies { ... annotationProcessor project(':apt-compiler-processor') implementation project(':apt-api')}
4.5 使用如下:
public class MainActivity extends AppCompatActivity { @BindView(value = R.id.test_textview) public TextView testTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnifeTest.getInstance().inject(this); testTextView.setText("手写 butterknife demo"); }}
运行代码截图
五、痛点及优化
我们可以看到,按官方文档一步步来写apt比较繁琐,
- 生成代码时需要字符串拼接,代码量多的时候容易出错,排查困难
- 需要继承AbstrctProcessor并重写多个方法,写入注解容易遗漏
- 注册APT的步骤繁琐,需手动创建文件
针对以上问题:
我们可以使用JavaPoet来替代拼接字符串( JavaPoet是一个用来生成Java代码的框架,对JavaPoet不了解的请自行学习)
官网地址: GitHub - square/javapoet: A Java API for generating .java source files.
使用Auto-Service来自动注册APT
这是谷歌官方出品的一个开源库,可以省去注册APT的步骤,只需要一行注释
先在apt-compiler模块中添加依赖
dependencies { ... implementation 'com.google.auto.service:auto-service:1.0-rc2'}
然后添加注释即可,如下图所示:
六、一些疑问
1 手写注解处理器时,注解处理器processor为什么要在META-INFO注册?
META-INFO相当于一个信息包,用于存放一些meta information相关的信息,用来配置应用程序、扩展程序、类加载器和服务manifest.mf文件,在编译时,java编译器回去该文件中查找实现了AbstractProcess的子类,就相当于注册。
2 APT(Annotation Processing Tool)如何调用AbstractProcess的呢?(注解处理器是如何被系统调用的?)
annotationProcessor 指定apt处理器。
创建一个类并继承自
AbstractProcessor
通过注解
@AutoService(Processor.class)
将创建的新类注册为APT
处理器。也可以手动创建,详细见下面demo说明
在
build.gradle
文件或项目的构建文件中指定APT
处理器。
annotationProcessor project(':apt-compiler-processor')
运行项目来激活
APT
处理器并让其开始处理。在 Eclipse 中你可以设置
Java Compiler > Annotation Processing
选项卡中的Enable annotation processing
复选框来启用它,在 Intellij IDEA 中,你需要选择菜单中的
"Build" > "Rebuild project"
来刷新生成器任务列表。完成这些步骤后,就可以开始使用AbstractProcessor
创建自己的注解处理器。在安卓中,直接构建项目即可。
3 安卓中,APT项目会不会增加apk的体积?
不会,processor的作用是在编译器解析注解、生成文件等,只在编译器用到,是不会打包进apk的。
更高级的用法可自行阅读开源项目,向大佬学习~
Demo 下载 :https://download.csdn.net/download/fumeidonga/87767415
来源地址:https://blog.csdn.net/fumeidonga/article/details/130532652
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341