简介
APT+JavaPoet 是一把利剑,可以将很多模板代码在编译期间直接生成,即通过注解收集信息,然后将这些信息形成一些固定代码;特别是在写框架的时候,可以将一些“脏活、累活”通过这种方式处理掉,然后提供给用户一个干净的API接口使用,目前常用在
一些复杂类型的Adapter 等等,这些都可以找到相关的开源库
关于APT+JavePoet要是不熟悉的话建议先看看我之前的注解系列
注解系列
一些技巧
老生常谈的面向接口编程
比如在MPermissions中,提供了
1 | public interface PermissionProxy<T> { |
然后APT生成的代码实现该接口
1 |
|
实际逻辑操作中直接使用该接口
1 | public static boolean shouldShowRequestPermissionRationale(Activity activity, String permission, int requestCode) { |
ARouter(Arouter解析)中也有这种处理的方式,比如IRouteGroup
LogisticsCenter#completion
1 | IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance(); |
思想一样这里就不展开赘述了
信息注入分离
比如在ARouter中有一个仓库
WearHouse类里面就是一些空壳容器,用来盛放路由的元信息,
1 | class Warehouse { |
我们知道具体的信息是在注解之中,APT+JavaPoet负责将信息收集,在ARouter中体现如下
1 |
|
可以看出只有一个接受注入的信息的函数,然后在实际逻辑处理中,将此处的信息load到WareHouse中对应map
LogisticsCenter#completion
1 | IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance(); |
干净清爽
常用的JavaPoet
基本操作可以查看官方文档JavaPoet 这里讲一下一些难点所在不过一般都是纠结在 获取类、接口、Map、带泛型的Map,下面将一一说明
获取类
有两种方式
ClassName.bestGuess(“类全名称”) 返回ClassName对象,这里的类全名称表示的类必须要存在,会自动导入相应的包
ClassName.get(“包名”,”类名”) 返回ClassName对象,不检查该类是否存在
因此需要注意获取类全名的类在以后重构时候改名类名
或者移动了位置
需要对应修改这里
占位符
- $L 字面常量(Literals)
- $S 字符串常量(String)
- $T 类型(Types)
该占位符最大特点就是会自动导包 - $N 命名(Names),通常指我们自己生成的方法名或者变量名等等
复杂类型
稍微复杂点的类型 比如泛型 、Map之类的,需要了解下JavaPoet定义的几种专门描述类型的类
常见的有
分类 | 生成的类型 | JavaPoet 写法 | 也可以这么写 (等效的 Java 写法) |
---|---|---|---|
内置类型 | int | TypeName.INT | int.class |
数组类型 | int[] | ArrayTypeName.of(int.class) | int[].class |
需要引入包名的类型 | java.io.File | ClassName.get(“java.io”, “File”) | java.io.File.class |
参数化类型 (ParameterizedType | List | ParameterizedTypeName.get(List.class, String.class) | - |
类型变量 (WildcardType) 用于声明泛型 | T | TypeVariableName.get(“T”) | - |
通配符类型 | ? extends String | WildcardTypeName.subtypeOf(String.class) | - |
通过ARouter中的一段代码,就可以解释的很清楚
RouterProcessor
1 | //参数化类型 Map<String, Class<? extends IRouteGroup>> |
生成代码
1 |
|
或者直接先把你需要的泛型都写出来
1 | final ClassName java_lang_Class = ClassName.get(Class.class); |
然后在需要时直接拿到即可,这里是作为一个变量(Field使用)
1 | .addField(FieldSpec.builder(mapOfClassToSetOfClass, "sServices") |
要点分析
Element和TypeMirror
这个点还是非常重要的,我们的java代码在对于APT处理时只不过各种的Element的结构化文本,当我们需要进行细致的逻辑判断时候,比如是否是某个类的子类,就需要操作他们了
1 | package com.example; // PackageElement |
Element
代表java源文件中的程序构建元素,例如包、类、方法等。Element接口有5个子类。
PackageElement | 表示一个包程序元素,可以获取到包名等 |
---|---|
TypeParameterElement | 表示一般类、接口、方法或构造方法元素的泛型参数 |
TypeElement | 表示一个类或接口程序元素 |
VariableElement | 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 |
ExecutableElement | 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素 |
开发中Element可根据实际情况强转为以上5种中的一种
- 当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。
那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、方法名、参数类型、返回值,如何做?
1 |
|
- 当你有一个注解是以@Target(ElementType.FIELD)定义时,表示该注解只能修饰属性、类成员。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、类成员类型、类成员名,如何获取?
1 |
|
- 当你有一个注解是以@Target(ElementType.TYPE)定义时,表示该注解只能修饰类、接口、枚举。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、全类名、父类,如何获取?
1 |
|
Element代表的是源代码。TypeElement代表的是源代码中的类型元素,例如类。然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。TypeMirror
用与描述Java程序中元素的信息,即Elment的元信息。通过通过Element.asType()
接口可以获取Element的TypeMirror,结构比较复杂
常用的TypeMirror,如下
PrimitiveType | 原始数据类型,boolean,byte,short int,long,float,char,double |
---|---|
ReferenceType | 引用类型 |
ArrayType | 数组类型 |
DeclaredType | 声明的类型,例如类、接口、枚举、注解类型 |
AnnotationType | 注解类型 |
ClassType | 类类型 |
EnumType | 枚举类型 |
InterfaceType | 接口类型 |
TypeVariable | 类型变量类型 |
VoidType | void 类型 |
WildcardType | 通配符类型 |
当TypeMirror是DeclaredType或者TypeVariable时,TypeMirror可以转化成Element:
1 | Element element = processingEviroment.getTypeUtils().asElement(typeMirror); |
在ARouter中 为了区分是否是某个类的子类使用到了TypeMirro
RouterProcessor # parseRoutes
1 |
|
调试
- 使用Messager
Messager
提供给注解处理器一个报告错误、警告以及提示信息的途径。它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的。
在官方文档中描述了消息的不同级别中非常重要的是Kind.ERROR,因为这种类型的信息用来表示我们的注解处理器处理失败了。很有可能是第三方开发者错误的使用了注解。这个概念和传统的Java应用有点不一样,在传统Java应用中我们可能就抛出一个异常Exception。如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),使用我们注解处理器第三方开发者将会从javac中得到非常难懂的出错信息,因为它包含注解处理器的堆栈跟踪(Stacktace)信息。因此,注解处理器就有一个Messager类,它能够打印非常优美的错误信息。除此之外,你还可以连接到出错的元素。在像现在的IDE(集成开发环境)中,第三方开发者可以直接点击错误信息,IDE将会直接跳转到第三方开发者项目的出错的源文件的相应的行。
因此我们通常封装一个Logger去打印关键点,具体可以参考ARouter的Logger
- 断点调试
(1) 在项目的根目录下的gradle.properties文件中,新增如下配置:
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
org.gradle.daemon=true
(2)新建remote debugger
注意新建remoteDebuger的名称一定要是AnnotationProcessor
(3)Debug AnnotationProcessor
结语
APT + JavaPoet 固然比较强大,但是也有其局限性,比如它无法扫描 AAR、JAR包,在一些大型app上分模块最终以jar包形式提供的话,就不能扫描到注解了,那这时就需要借助于更为强大的技术了,可以通过自定义Gradle Plugin + JavaAssist在dex之前扫描class方式去生成我们想要的代码,这是后话了。