注解-谈谈APT和JavaPoet的一些使用技巧和要点 | 字数总计: 3.2k | 阅读时长: 13分钟 | 阅读量:
简介 APT+JavaPoet 是一把利剑,可以将很多模板代码在编译期间直接生成,即通过注解收集信息,然后将这些信息形成一些固定代码;特别是在写框架的时候,可以将一些“脏活、累活”通过这种方式处理掉,然后提供给用户一个干净的API接口使用,目前常用在
一些复杂类型的Adapter 等等,这些都可以找到相关的开源库
一些技巧 老生常谈的面向接口编程 比如在MPermissions 中,提供了
1 2 3 4 5 6 7 public interface PermissionProxy <T > { void grant (T source, int requestCode) ; void denied (T source, int requestCode) ; void rationale (T source, int requestCode) ; boolean needShowRationale (int requestCode) ; }
然后APT生成的代码实现该接口
1 2 3 4 5 public class MainActivity $$PermissionProxy implements PermissionProxy <MainActivity > { …… }
实际逻辑操作中直接使用该接口
1 2 3 4 5 6 7 8 9 10 11 public static boolean shouldShowRequestPermissionRationale (Activity activity, String permission, int requestCode) { PermissionProxy proxy = findPermissionProxy(activity); if (!proxy.needShowRationale(requestCode)) return false ; if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { proxy.rationale(activity, requestCode); return true ; } return false ; }
ARouter(Arouter解析 )中也有这种处理的方式,比如IRouteGroup LogisticsCenter#completion
1 2 3 IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance(); iGroupInstance.loadInto(Warehouse.routes); Warehouse.groupsIndex.remove(postcard.getGroup());
思想一样这里就不展开赘述了
信息注入分离 比如在ARouter中有一个仓库WearHouse类里面就是一些空壳容器,用来盛放路由的元信息,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Warehouse { static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>(); static Map<String, RouteMeta> routes = new HashMap<>(); static Map<Class, IProvider> providers = new HashMap<>(); static Map<String, RouteMeta> providersIndex = new HashMap<>(); static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]" ); static List<IInterceptor> interceptors = new ArrayList<>(); …… }
我们知道具体的信息是在注解之中,APT+JavaPoet负责将信息收集,在ARouter中体现如下
1 2 3 4 5 6 7 8 9 10 public class ARouter $$Group $$service implements IRouteGroup { @Override public void loadInto (Map<String, RouteMeta> atlas) { atlas.put("/service/hello" , RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello" , "service" , null , -1 , -2147483648 )); atlas.put("/service/json" , RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json" , "service" , null , -1 , -2147483648 )); atlas.put("/service/single" , RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single" , "service" , null , -1 , -2147483648 )); } }
可以看出只有一个接受注入的信息的函数,然后在实际逻辑处理中,将此处的信息load到WareHouse中对应map
LogisticsCenter#completion
1 2 3 IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance(); iGroupInstance.loadInto(Warehouse.routes); Warehouse.groupsIndex.remove(postcard.getGroup());
干净清爽
常用的JavaPoet 基本操作可以查看官方文档JavaPoet 这里讲一下一些难点所在不过一般都是纠结在 获取类、接口、Map、带泛型的Map,下面将一一说明
获取类 有两种方式
因此需要注意获取类全名的类在以后重构时候改名类名或者移动了位置需要对应修改这里
占位符
$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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ParameterizedTypeName.get( ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup)) ) ); ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ClassName.get(RouteMeta.class) ); ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes" ).build(); ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas" ).build();
生成代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ARouter $$Root $$app implements IRouteRoot { @Override public void loadInto (Map<String, Class<? extends IRouteGroup>> routes) { routes.put("service" , ARouter$$Group$$service.class); routes.put("test" , ARouter$$Group$$test.class); } } public class ARouter $$Group $$service implements IRouteGroup { @Override public void loadInto (Map<String, RouteMeta> atlas) { atlas.put("/service/hello" , RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello" , "service" , null , -1 , -2147483648 )); atlas.put("/service/json" , RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json" , "service" , null , -1 , -2147483648 )); atlas.put("/service/single" , RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single" , "service" , null , -1 , -2147483648 )); } }
或者直接先把你需要的泛型都写出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 final ClassName java_lang_Class = ClassName.get(Class.class); final ClassName java_util_Collections = ClassName.get("java.util" , "Collections" ); final ClassName java_util_Map = ClassName.get("java.util" , "Map" ); final ClassName java_util_Set = ClassName.get("java.util" , "Set" ); final ClassName java_util_LinkedHashMap = ClassName.get("java.util" , "LinkedHashMap" ); final ClassName java_util_LinkedHashSet = ClassName.get("java.util" , "LinkedHashSet" ); final ClassName instantiator = ClassName.get("java.util.concurrent" , "Callable" ); final TypeName classOfAny = ParameterizedTypeName.get(java_lang_Class, any); final TypeName instantiatorOfAny = ParameterizedTypeName.get(instantiator, any); final TypeName instantiatorOfP = ParameterizedTypeName.get(instantiator, p); final TypeName classOfS = ParameterizedTypeName.get(java_lang_Class, s); final TypeName classOfP = ParameterizedTypeName.get(java_lang_Class, p); final TypeName classOfSubTypeOfS = ParameterizedTypeName.get(java_lang_Class, subTypeOfS); final TypeName setOfClass = ParameterizedTypeName.get(java_util_Set, classOfAny); final TypeName setOfClassOfSubTypeOfS = ParameterizedTypeName.get(java_util_Set, classOfSubTypeOfS); final TypeName linkedHashSetOfClass = ParameterizedTypeName.get(java_util_LinkedHashSet, classOfAny); final TypeName mapOfClassToSetOfClass = ParameterizedTypeName.get(java_util_Map, classOfAny, setOfClass); final TypeName mapOfClassToInstantiator = ParameterizedTypeName.get(java_util_Map, classOfAny, instantiatorOfAny); final TypeName linkedHashMapOfClassToSetOfClass = ParameterizedTypeName.get(java_util_LinkedHashMap, classOfAny, setOfClass); final TypeName linkedHashMapOfClassToInstantializer = ParameterizedTypeName.get(java_util_LinkedHashMap, classOfAny, instantiatorOfAny);
然后在需要时直接拿到即可,这里是作为一个变量(Field使用)
1 2 3 4 5 6 7 8 9 10 .addField(FieldSpec.builder(mapOfClassToSetOfClass, "sServices" ) .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) .initializer("new $T()" , linkedHashMapOfClassToSetOfClass) .build()) .addField(FieldSpec.builder(mapOfClassToInstantiator, "sInstantiators" ) .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) .initializer("new $T()" , linkedHashMapOfClassToInstantializer) .build())
要点分析 Element和TypeMirror 这个点还是非常重要的,我们的java代码在对于APT处理时只不过各种的Element的结构化文本,当我们需要进行细致的逻辑判断时候,比如是否是某个类的子类,就需要操作他们了
1 2 3 4 5 6 7 8 9 10 11 12 package com.example; // PackageElement public class Test { // TypeElement private int a; // VariableElement private Test other; // VariableElement public Test () {} // ExecuteableElement public void setA ( // ExecuteableElement int newA // TypeElement ) {} }
Element代表java源文件中的程序构建元素,例如包、类、方法等。Element接口有5个子类。
PackageElement
表示一个包程序元素,可以获取到包名等
TypeParameterElement
表示一般类、接口、方法或构造方法元素的泛型参数
TypeElement
表示一个类或接口程序元素
VariableElement
表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
ExecutableElement
表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素
开发中Element可根据实际情况强转为以上5种中的一种
当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。 那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、方法名、参数类型、返回值,如何做?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 for (Element element : roundEnv.getElementsAnnotatedWith(Inject.class)) { ExecutableElement executableElement = (ExecutableElement) element; TypeElement classElement = (TypeElement) element .getEnclosingElement(); Elements elementUtils = processingEnv.getElementUtils(); PackageElement packageElement = elementUtils.getPackageOf(classElement); String fullClassName = classElement.getQualifiedName().toString(); String className = classElement.getSimpleName().toString(); String packageName = packageElement.getQualifiedName().toString(); String methodName = executableElement.getSimpleName().toString(); List<? extends VariableElement> methodParameters = executableElement.getParameters(); List<String> types = new ArrayList<>(); for (VariableElement variableElement : methodParameters) { TypeMirror methodParameterType = variableElement.asType(); if (methodParameterType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) methodParameterType; methodParameterType = typeVariable.getUpperBound(); } String parameterName = variableElement.getSimpleName().toString(); String parameteKind = methodParameterType.toString(); types.add(methodParameterType.toString()); } }
当你有一个注解是以@Target(ElementType.FIELD)定义时,表示该注解只能修饰属性、类成员。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、类成员类型、类成员名,如何获取?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 for (Element element : roundEnv.getElementsAnnotatedWith(IdProperty.class)) { VariableElement variableElement = (VariableElement) element; TypeElement classElement = (TypeElement) element .getEnclosingElement(); PackageElement packageElement = elementUtils.getPackageOf(classElement); String className = classElement.getSimpleName().toString(); String packageName = packageElement.getQualifiedName().toString(); String variableName = variableElement.getSimpleName().toString(); TypeMirror typeMirror = variableElement.asType(); String type = typeMirror.toString(); }
当你有一个注解是以@Target(ElementType.TYPE)定义时,表示该注解只能修饰类、接口、枚举。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、全类名、父类,如何获取?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) { TypeElement classElement = (TypeElement) element; PackageElement packageElement = (PackageElement) element .getEnclosingElement(); String fullClassName = classElement.getQualifiedName().toString(); String className = classElement.getSimpleName().toString(); String packageName = packageElement.getQualifiedName().toString(); String superClassName = classElement.getSuperclass().toString(); }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 TypeMirror type_Activity = elements.getTypeElement(ACTIVITY).asType(); TypeMirror type_Service = elements.getTypeElement(SERVICE).asType(); TypeMirror fragmentTm = elements.getTypeElement(FRAGMENT).asType(); TypeMirror fragmentTmV4 = elements.getTypeElement(Consts.FRAGMENT_V4).asType(); if (types.isSubtype(tm, type_Activity)) { …… } else if (types.isSubtype(tm, iProvider)) { …… } else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) { …… }
调试
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方式去生成我们想要的代码,这是后话了。
参考