9.2APT技术设计详解
目录介绍
- 01.什么是apt
- 02.基本概念
- 2.1 注解处理器
- 2.2 抽象处理器
- 03.android-apt被替代
- 04.实际案例操作
- 4.1 实现目标
- 4.2 自定义注解
- 4.3 创建注解处理器
- 4.4 具体案例分享
01.什么是apt
- 什么是apt
- APT,就是Annotation Processing Tool的简称,叫做注解处理工具。就是可以在代码编译期间对注解进行处理,并且生成Java文件,减少手动的代码输入。注解我们平时用到的比较多的可能会是运行时注解,比如大名鼎鼎的retrofit就是用运行时注解,通过动态代理来生成网络请求。编译时注解平时开发中可能会涉及的比较少,但并不是说不常用,比如我们经常用的轮子Dagger2, ButterKnife, EventBus3 都在用。
- 编译时注解。
- 也有人叫它代码生成,其实他们还是有些区别的,在编译时对注解做处理,通过注解,获取必要信息,在项目中生成代码,运行时调用,和直接运行手写代码没有任何区别。而更准确的叫法:APT - Annotation Processing Tool
- 大概原理
- Java API 已经提供了扫描源码并解析注解的框架,开发者可以通过继承 AbstractProcessor 类来实现自己的注解解析逻辑。APT 的原理就是在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,并且自动调用其 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素在编译期输出对应的 Java 代码
02.基本概念
2.1 注解处理器
- 注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器。
- 注解处理器可以生成Java代码,这些生成的Java代码会组成 .java 文件,但不能修改已经存在的Java类(即不能向已有的类中添加方法)。而这些生成的Java文件,会同时与其他普通的手写Java源代码一起被javac编译。
2.2 抽象处理器
- 每一个注解处理器都要继承于AbstractProcessor,如下所示:
public class MyProcessor extends AbstractProcessor{ @Override public synchronized void init(ProcessingEnvironment processingEnvironment){ super.init(processingEnvironment); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){ return false; } @Override public Set<String> getSupportedAnnotationTypes(){ return super.getSupportedAnnotationTypes(); } @Override public SourceVersion getSupportedSourceVersion(){ return super.getSupportedSourceVersion(); } }
- 这几个方法如下
- init(ProcessingEnvironment processingEnvironment): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。后面我们将看到详细的内容。
- process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
- getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
- getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7的话,你也可以返回SourceVersion.RELEASE_7。我推荐你使用前者。
03.android-apt被替代
- annotationProcessor替代者
- Android Gradle 插件 2.2 版本的发布,之前 android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约三年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。
- annotationProcessor和apt区别?
- Android 官方的 annotationProcessor 同时支持 javac 和 jack 编译方式,而 android-apt 只支持 javac 方式。当然,目前 android-apt 在 Android Gradle 插件 2.2 版本上面仍然可以正常运行,如果你没有想支持 jack 编译方式的话,可以继续使用 android-apt。
- 什么是jack编译方式?
- Jack (Java Android Compiler Kit)是新的Android 编译工具,从Android 6.0 开始加入,替换原有的编译工具,例如javac, ProGuard, jarjar和 dx。它主要负责将java代码编译成dex包,并支持代码压缩,混淆等。
- Jack工具的主要优势
- 完全开放源码,源码均在AOSP中,合作伙伴可贡献源码
- 加快编译源码,Jack 提供特殊的配置,减少编译时间:pre-dexing, 增量编译和Jack编译服务器.
- 支持代码压缩,混淆,重打包和multidex,不在使用额外单独的包,例如ProGuard。
04.实际案例操作
4.1 实现目标
- 通过一个栗子来进行分析。平时一般启动Activity都是这样通过startActivity或者startActivityForResult等等balabala。今天我们通过给Activity添加一个注解@RouteAnnotation(name = "RouteName_Activity"),然后通过注解来启动Activity,AnnocationRouter.getSingleton().navigat("RouteName_Activity")。
4.2 自定义注解
- 定义注解RouteAnnotation,作用对象就是类,作用范围就是编译时。然后就是接受一个参数,也就是Activity的别名。
/** * <pre> * @author 杨充 * blog : https://github.com/yangchong211 * time : 2018/05/21 * desc : 定义注解RouteAnnotation,作用对象就是类,作用范围就是编译时 * revise: 接受一个参数,也就是Activity的别名 * </pre> */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface RouteAnnotation { String name(); }
4.3 创建注解处理器
- 注解处理器一般会是一个项目比较底层的模块,因此我们创建一个Java Library,annotationprocess模块,要依赖前面的注解。自定义的处理器需要继承AbstractProcessor,需要自己实现process方法。
- 注意,这里是创建Java Library工程
import javax.annotation.processing.AbstractProcessor; public class AnnotationProcessor extends AbstractProcessor { private Filer mFiler; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mFiler = processingEnvironment.getFiler(); } @Override public SourceVersion getSupportedSourceVersion() { //getSupportedSourceVersion()方法返回 Java 版本 return SourceVersion.latestSupported(); } @Override public Set<String> getSupportedAnnotationTypes() { //getSupportedAnnotationTypes()方法返回要处理的注解的集合 LinkedHashSet<String> types = new LinkedHashSet<>(); types.add(RouteAnnotation.class.getCanonicalName()); return types; } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { HashMap<String, String> nameMap = new HashMap<>(); Set<? extends Element> annotationElements = roundEnvironment.getElementsAnnotatedWith(RouteAnnotation.class); for (Element element : annotationElements) { RouteAnnotation annotation = element.getAnnotation(RouteAnnotation.class); String name = annotation.name(); nameMap.put(name, element.getSimpleName().toString()); //nameMap.put(element.getSimpleName().toString(), name);//MainActiviy-RouteName_MainActivity } //generate Java File generateJavaFile(nameMap); return true; } private void generateJavaFile(Map<String, String> nameMap) { //generate constructor MethodSpec.Builder constructorBuidler = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addStatement("routeMap = new $T<>()", HashMap.class); for (String key : nameMap.keySet()) { String name = nameMap.get(key); constructorBuidler.addStatement("routeMap.put(\"$N\", \"$N\")", key, name); } MethodSpec constructorName = constructorBuidler.build(); //generate getActivityRouteName method MethodSpec routeName = MethodSpec.methodBuilder("getActivityName") .addModifiers(Modifier.PUBLIC) .returns(String.class) .addParameter(String.class, "routeName") .beginControlFlow("if (null != routeMap && !routeMap.isEmpty())") .addStatement("return (String)routeMap.get(routeName)") .endControlFlow() .addStatement("return \"\"") .build(); //generate class TypeSpec typeSpec = TypeSpec.classBuilder("AnnotationRoute$Finder") .addModifiers(Modifier.PUBLIC) .addMethod(constructorName) .addMethod(routeName) .addSuperinterface(Provider.class) .addField(HashMap.class, "routeMap", Modifier.PRIVATE) .build(); JavaFile javaFile = JavaFile.builder("com.ycbjie.video.annotaioncompiletest", typeSpec).build(); try { javaFile.writeTo(mFiler); } catch (IOException e) { e.printStackTrace(); } } }
- 重点就是process方法,方法里面主要工作就是生成Java文件。我们具体看下步骤:
- 1.roundEnvironment.getElementsAnnotatedWith(RouteAnnotation.class)拿到所有RouteAnnotation注解标注的类
- 2.循环取出注解的name属性和被标注的类名并缓存,其实就是:
- put("RouteName_ActivityTwo", "ActivityTwo");
- 3.通过javapoet库生成Java类,javapoet是square公司良心出品,让我们脱离手动凭借字符串来生成Java类的痛苦,可以通过各种姿势来生成Java类,这里不多做介绍,有需要的可以看官方文档,很详细。
- 4.最后通过JavaFile.builder("包名",TypeSpec)生成Java文件,包名可以随意取,最后生成的文件都是在主程序模块app.build.generated.source.apt目录下
03.Element
- Element也是APT的重点之一,所有通过注解取得元素都将以Element类型等待处理,也可以理解为Element的子类类型与自定义注解时用到的@Target是有对应关系的。
- Element的官方注释:Represents a program element such as a package, class, or method. Each element represents a static, language-level construct (and not, for example, a runtime construct of the virtual machine).
- 表示一个程序元素,比如包、类或者方法。
- Element的子类有,不同Element的信息获取方式不同。
ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。 对应@Target(ElementType.METHOD) @Target(ElementType.CONSTRUCTOR) PackageElement; 表示一个包程序元素。提供对有关包极其成员的信息访问。 对应@Target(ElementType.PACKAGE) TypeElement; 表示一个类或接口程序元素。提供对有关类型极其成员的信息访问。 对应@Target(ElementType.TYPE) 注意:枚举类型是一种类,而注解类型是一种接口。 TypeParameterElement; 表示一般类、接口、方法或构造方法元素的类型参数。 对应@Target(ElementType.PARAMETER) VariableElement; 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。 对应@Target(ElementType.LOCAL_VARIABLE)
- 例如:取得所有修饰了@XXX的元素。
for (Element element : roundEnv.getElementsAnnotatedWith(XXX.class)){ //OnceClick.class是@Target(METHOD) //则该element是可以强转为表示方法的ExecutableElement ExecutableElement method = (ExecutableElement)element; //如果需要用到其他类型的Element,则不可以直接强转,需要通过下面方法转换 //但有例外情况,我们稍后列举 TypeElement classElement = (TypeElement) element .getEnclosingElement(); }
04.修饰方法的注解和ExecutableElement
- 当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、方法名、参数类型、返回值。
- 代码如下所示
//xxx.class 以 @Target(ElementType.METHOD)修饰 for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) { //对于Element直接强转 ExecutableElement executableElement = (ExecutableElement) element; //非对应的Element,通过getEnclosingElement转换获取 TypeElement classElement = (TypeElement) element .getEnclosingElement(); //当(ExecutableElement) element成立时,使用(PackageElement) element // .getEnclosingElement();将报错。 //需要使用elementUtils来获取 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()); } }
- 代码如下所示
05.修饰属性、类成员的注解和VariableElement
- 当你有一个注解是以@Target(ElementType.FIELD)定义时,表示该注解只能修饰属性、类成员。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、类成员类型、类成员名。
- 代码如下所示
for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) { //ElementType.FIELD注解可以直接强转VariableElement 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(); }
- 代码如下所示
06.修饰类的注解和TypeElement
- 当你有一个注解是以@Target(ElementType.TYPE)定义时,表示该注解只能修饰类、接口、枚举。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、全类名、父类。
- 代码如下所示
//遍历项目中所有的xxx注解 for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) { //ElementType.TYPE注解可以直接强转TypeElement 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(); }
- 代码如下所示