vlambda博客
学习文章列表

peanut系列: APT技术的介绍、应用和Android Studio上手实操


peanut文章系列前言想平时抽空做一个关于开源项目的系列文章。原本打算从Android-Dagger2的设计思想以及源码做起,结果中途遇到不少细节问题,比如自定义注解处理器。其实扫一眼也就过去了,注解谁还没用过。但是正好遇到过自定义注解处理器,就停下来仔细折腾了一下,不得不说还真挺磨人。于是整理一下有关“Java自定义注解处理器”的知识、应用和实践。

(小声吐槽 网上一搜索虽然条目很多,但是很少有真正能所谓“一文读懂”的文章,读者不得不需要去自行缝合,甚至很多文章内容大体相似,令人开始怀疑是不是AI技术发展得已经能生成技术类营销号文章了,而且讲真搜索引擎怎么感觉越来越辣眼睛了。)



摘要: 通过阅读本文,你主要能收获:1.我们能用APT技术干什么?2.2021年了我怎么在Android Studio上编写我自己的注解处理器。3.解答“为什么我的注解处理器不起作用?为什么在build/generated/apt里没有生成的文件?”等坑。从文章结构上来说,本文首先介绍了Java注解的诞生,以及它在那些应用场景能给我们带来便捷。第二节简要回顾了一些笔者认为重要的相关概念知识,并提供了参考来源供深入了解。第三节从Android开发角度对《on Java 8》里的小案例进行了踩坑实践。


关键词: 注解Annotation, 注解处理器Annotation Processor, APT(Annotation Processing Tool), Dagger2, API管控



1 背景
2004年9月30日Java5发布,伴随着许多重大更新,包括泛型支持、基本类型的自动装箱等,注解(Annotation)机制的引入似乎是不那么起眼。然而发展到现在,APT技术往往关联着项目整体框架的设计,无论是直接相关的Dagger2, 还是EventBus3, Retrofit等开源项目都与APT技术(Annotation Processing Tool)有着或多或少的联系,可以说基于Java注解的APT技术已经成了优秀项目中重要组成部分。从我个人在移动端开发的经验来说,在移动端开发中常常有双端对齐的需求,而Android因为有Java注解机制总能比OC省力不少(很多)。

1.1什么是Java注解(Annotation)
《on Java 8》写到: “注解是 Java 引入的一项非常受欢迎的补充,它提供了一种结构化,并且具有类型检查能力的新途径,从而使得你能够为代码中加入元数据(解释数据的数据),而且不会导致代码杂乱并难以阅读。”可以说从写代码的角度,注解@annotation的功能和地位是和class, interface平级的。

1.2注解给我们带来了什么
《on Java 8写到: 通过使用注解,你可以将元数据保存在 Java 源代码中。并拥有如下优势: 简单易读的代码,编译器类型检查,使用 annotation API 为自己的注解构造处理工具。即使 Java 定义了一些类型的元数据,但是一般来说注解类型的添加和如何使用完全取决于你。“

实际上注解基于“描述”这个本质出发能够带来的更多想象空间。如果能够有一套基于好的业务应用抽象的注解机制,无论是第三方库(Dagger2, JUnit)或者是你根据业务场景开发的,都能极大提高我们的开发体验、开发效率,并能增加管控手段,形成一套自动化方案,比如文档化工具,单元测试框架,依赖注入框架等等。

1.2.1 开发时: 部署描述性文件
注解本身就能够帮助我们避免编写累赘的部署描述性文件。而 Javadoc 中的 @deprecated @Deprecated 注解所替代的事实也说明,与注释性文字相比,注解绝对更适用于描述类相关的信息。每当创建涉及重复工作的类或接口时,你通常可以使用注解来自动化和简化流程。例如在 Enterprise JavaBean(EJB)中的许多额外工作就是通过注解来消除的。

1.2.2 编译时: 自动生成文件
其次我们通过在编译时处理注解,还可以达到自动生成文件的效果。比如butterknife、 Dagger2等。这个应用场景我们会在第三节的案例中详细描述。

1.2.3 运行时: 结合反射进行动态代理
通过将注解文件保留到运行时,在运行时结合Java反射机制以及动态代理设计模式,能够很好的去进行解耦,代价就是会对性能有影响。比如单元测试框架。可以看看反射注解与动态代理综合使用》。

1.2.4 一个实际业务场景案例: API管控
(这一小节属于重点之外的拓展,写得比较简略,没有相关经验的同学可能没有类似体会,也是没关系的。)

基础库业务经常有API管控的需求,这是因为如果一开始API设计不合理就需要下线、替换、升级(这里API是泛指,对于基础库而言,提供方法、类、甚至规范标准是常有的)。虽然业务开发者根本不想也不应该关心基础库内部的实现,但是无论从业务发展还是从技术迭代的角度,从一开始就能设计好API并能持续使用几年十几年,在实际中几乎是不存在的事。因此无论是不是“屎山”代码,基础库技术到了时间去升级重构是必要的,否则真的成为屎山一般难以维护,令所有人汗颜。

然而如果只通过标记过期@Deprecated效率太低,并且实际操作上难以推动,反之如果能够知道具体是哪些业务组件继承了基础库的API,以及他们具体是怎么使用这些API的,我们就能更高效、更好地做抽象,有针对性地推动业务升级提供新的API。

API管控角度,可以分为添加注解或者静态代码分析AST(Abstract Syntax Tree抽象语法树)两种手段,区别在于添加注解的手段是在开发阶段,而静态代码分析AST的手段是在打包阶段。对于多业务组件依赖的基础库业务而言,注解机制实际上对于业务适配的成本是比较高的,推动起来会很困难。如果从静态代码管控会迅速高效,但是实际上缺少注解的扫代码往往会错扫漏扫。因此最佳理想方案是注解机制和编译阶段分析AST结合起来,达到预期效果,做得好的话既能高效推动,好的注解机制也能减少之后的维护成本。

2 相关知识概念回顾
建议有时间有兴趣的同学还是看官方文档或者大牛书籍详细了解一下,这里主要介绍和本次讨论内容有关的知识。

2.1 元注解-定义注解的注解
Java中目前有5种标准注解: 
@Override, @Deprecated, @SuppressWarnings, @SafeVarargs, @FunctionalInterface
以及5种元注解: 
@Target, @Retention, @Documented, @Inherited, @Repeatable
元注解就是帮助我们完成自定义注解的注解工具, 其中我们在自定义注解的时候主要会用到@Target和@Retention, 因此这里重点介绍一下这两个元注解。

2.1.1 @Target
Target顾名思义是我们自定义注解的目标对象,表示我自定义注解适用的范围。可以用的值有以下几种:
ElementType.CONSTRUCTOR: 构造器的声明
ElementType.FIELD: 字段声明(包括 enum 实例)
ElementType.LOCAL_VARIABLE: 局部变量声明
ElementType.METHOD: 方法声明
ElementType.PACKAGE: 包声明
ElementType.PARAMETER: 参数声明
ElementType.TYPE: 类、 接口(包括注解类型)或者 enum 声明

2.1.2 @Retention
Retention英文的意思是保留,这样就很容易理解了,表示我们的自定义注解希望保留到什么时候,Retention的值可以有以下三种:
RetentionPolicy.SOURCE: 注解将被编译器丢弃
RetentionPolicy.CLASS: 注解在class文件中可用,但是会被VM丢弃
RetentionPolicy.RUNTIME: VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息

可以看出来,@Target和@Retention实际上一个从空间,一个从时间,建立规范了我们的自定义注解。

2.2 反射+注解+动态代理
这部分文章《反射注解与动态代理综合使用》写得已经很好了,我简单给Java初学者补充一下反射的用途。

大家都知道通过反射可以在运行时拿到类的构造方法成员方法/字段和构造函数。但是Java初学者可能会问这有什么意义呢?比如我已经能new一个Apple类了,为什么要反射出Apple类拿到呢?

应用场景是比如你上线了一个“获取水果App”,但是你不知道用户在线上想要Apple还是Orange还是别的,这时候就可以在开发时运用反射+动态代理,App就能在线上(运行时)基于用户给出的信息反射出用户想要的水果实例。这是new是无法在运行时做到的。

3 实践
下面的例子就是《on Java 8》里的原例,这个例子的目的是基于我们的自定义注解,能够通过自定义注解处理器自动抓取被注解标记的类里的方法,生成一个interface类。篇幅原因,理解这个例子还是请大家结合on Java 8》吧,这里主要记录一下踩坑过程。
虽然是原例,但是在Android Studio里面运行还是会踩坑,在坑里快一整个下午。所以记录一下实践过程,并且介绍一下每个步骤都在做些什么。 (吐槽二度 peanut系列: APT技术的介绍、应用和Android Studio上手实操 : 感觉搜索引擎真是越来越辣鸡了。

注明一下时间日期和环境,这个很重要!不谈版本环境的都是泪啊。

实践开发环境说明
操作时间 2021.08.29
Android Studio版本 4.1.3
Android SDK版本

compileSdkVersion 30

buildToolsVersion "30.0.3"

Android Gradle Plugin Version
4.1.3
Gradle Version
6.5
Java版本

sourceCompatibility JavaVersion.VERSION_1_8

targetCompatibility JavaVersion.VERSION_1_8

AutoService依赖版本
com.google.auto.service:auto-service:1.0-rc7

3.1 准备好我们的项目结构
从Android Studio里面新建Empty Activity,设置好后新建两个Java Library Module。最终项目结构如下:
- annotation模块: 这里放自定义的注解
- annotationprocessor模块: 这里放自定义注解处理器
- app模块: 这里放我们的demo,可以使用我们的自定义注解,也可以使用我们的处理器生成的文件
       peanut系列: APT技术的介绍、应用和Android Studio上手实操

3.2 编写注解
     peanut系列: APT技术的介绍、应用和Android Studio上手实操        
package com.runuts.android.annotation;
import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;
@Target({ElementType.TYPE})@Retention(RetentionPolicy.CLASS)public @interface ExtractInterface { String interfaceName() default "-!!-";}

3.3 编写注解处理器
  peanut系列: APT技术的介绍、应用和Android Studio上手实操        
package com.runuts.android.annotationprocessor;
import com.google.auto.service.AutoService;import com.runuts.android.annotation.ExtractInterface;
import javax.annotation.processing.*;import javax.lang.model.SourceVersion;import javax.lang.model.element.*;import javax.lang.model.util.*;import java.util.*;import java.util.stream.*;import java.io.*;
@SupportedAnnotationTypes("com.runuts.android.annotation.ExtractInterface")@SupportedSourceVersion(SourceVersion.RELEASE_8)@AutoService(Processor.class)public class IfaceExtractorProcessor extends AbstractProcessor {
private ArrayList<Element> interfaceMethods = new ArrayList<>(); Elements elementUtils; private ProcessingEnvironment processingEnv;
@Override public void init(ProcessingEnvironment processingEnv) { this.processingEnv = processingEnv; elementUtils = processingEnv.getElementUtils(); }
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { for(Element elem:env.getElementsAnnotatedWith(ExtractInterface.class)) { String interfaceName = elem.getAnnotation( ExtractInterface.class).interfaceName(); for(Element enclosed : elem.getEnclosedElements()) { if(enclosed.getKind() .equals(ElementKind.METHOD) && enclosed.getModifiers() .contains(Modifier.PUBLIC) && !enclosed.getModifiers() .contains(Modifier.STATIC)) { interfaceMethods.add(enclosed); } }
if(interfaceMethods.size() > 0) writeInterfaceFile(interfaceName); } return false; }
private void writeInterfaceFile(String interfaceName) { try( Writer writer = processingEnv.getFiler() .createSourceFile(interfaceName) .openWriter() ){ String packageName = elementUtils .getPackageOf(interfaceMethods .get(0)).toString(); writer.write( "package " + packageName + ";\n"); writer.write("public interface " + interfaceName + " {\n"); for(Element elem : interfaceMethods) { ExecutableElement method = (ExecutableElement)elem; String signature = " public "; signature += method.getReturnType() + " "; signature += method.getSimpleName(); signature += createArgList(method.getParameters()); System.out.println(signature); writer.write(signature + ";\n"); } writer.write("}"); } catch(Exception e) { throw new RuntimeException(e); } }
private String createArgList( List<? extends VariableElement> parameters) { String args = parameters.stream() .map(p -> p.asType() + " " + p.getSimpleName()) .collect(Collectors.joining(", ")); return "(" + args + ")"; }}

3.4使用注解
           
package com.runuts.android.aptdemo;
import com.runuts.android.annotation.ExtractInterface;
@ExtractInterface(interfaceName="IMultiplier")public class Multiplier {public boolean flag = false;private int n = 0;
public int multiply(int x, int y) {int total = 0;for(int i = 0; i < x; i++) total = add(total, y);return total; }
public int fortySeven() { return 47; }
private int add(int x, int y) {return x + y; }
public double timesTen(double arg) { return arg * 10; }
public static void main(String[] args) { Multiplier m = new Multiplier(); System.out.println( "11 * 16 = " + m.multiply(11,16)); }}

3.5 构建依赖关系
参考 编译时注解(APT) — 自定义注解处理器

3.5.1 (推荐)Google工具AutoService
使用Google的AutoService工具,在annotationprocessor模块里的build.gradle里添加dependencies里的倒数两行。注意版本。

// annotationprocessor - build.gradleplugins { id 'java-library'}
java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8}
dependencies {   implementation project(path: ':annotation') // 谷歌的 AutoService 可以让我们的注解处理器自动注册上 implementation 'com.google.auto.service:auto-service:1.0-rc7' // 这个一定要添加,否则是无法在app/build/generated/ap_generated_sources/debug/out生成文件的 annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'}

而在app模块的build.gradle里面需要依赖上我们自己的annotationProcessor
// app - build.gradledependecies { annotationProcessor project(':annotationprocessor') implementation project(path: ':annotation')}

3.5.2 (不推荐)手动通过创建META-INF注册
手动已经不推荐了,并且会踩坑。感兴趣的可以了解一下原理,但是建议别用了。
手动案例: the-10-step-guide-to-annotation-processing-in-android-studio

3.6 Make Project,Voilà !
接下来就make proejct或者rebuild就好了。
               
可以看到在路径
“aptdemo/app/build/generated/ap_generated_sources/debug/out/”
里面生成了IMultiplier.java文件
package com.runuts.android.aptdemo;public interface IMultiplier {public int multiply(int x, int y);public int fortySeven();public double timesTen(double arg);}

4 相关内容
4.1 关于坑点记录
一整套下来好像顺顺利利,但其实还是花了很多时间踩坑的。

坑1: 为什么我的注解处理器就是不生成文件。
查找了半天,网上有说是要降低gradle版本的,果不其然,试了一下还真行。但是降低版本也是有点离谱。继续查发现可以升级autoService版本。
https://blog.csdn.net/BunnyCoffer/article/details/108598128

坑2: 为什么我升级版本了还是没有文件?
升级完之后还是找不到?因为生成的文件在其他地方。
“aptdemo/app/build/generated/ap_generated_sources/debug/out/”

坑3: RetentonPolicy.SOURCE和RetentonPolicy.CLASS究竟有什么区别
看一看下面这个文章,简单来说就是对于Android开发来说,因为Android系统封装了Class文件之后的过程,所以从应用层面来讲没什么区别。
深入理解编译注解(五)RetentionPolicy.SOURCE 和 RetentionPolicy.CLASS区别讨论

4.2 拓展
APT技术除了上面的生成代码,动态加载等以外,还可以修改代码,例如一些代码插桩框架、日志框架、方法耗时统计框架等。可以看下面这篇文章。
自定义 Gradle 插件在编译时修改代码 


其他参考文章列表
《on Java 8
反射注解与动态代理综合使用
Android模块开发之APT技术
https://zhuanlan.zhihu.com/p/92724867
Android Studio写注解器
自定义注解处理器生成代码
自定义 Gradle 插件在编译时修改代码 
深入理解编译注解(五)RetentionPolicy.SOURCE 和 RetentionPolicy.CLASS区别讨论