注解与APT注解处理器技术详解

知识点汇总:

一:什么是注解

二:注解处理器概述

三:注解处理器核心类解析

四:如何调试注解处理器

五:通过注解处理器动态生成代码

六:问题汇总

七:扩展阅读


一:什么是注解

官方定义:

       注释是元数据的一种形式,提供有关程序的数据,该数据不属于程序本身。注释对其注释的代码的操作没有直接影响。注释有多种用途,其中包括: 编译器信息——编译器可以使用注释来检测错误或抑制警告。编译时和部署时处理——软件工具可以处理注释信息以生成代码、XML 文件等。运行时处理——一些注解可以在运行时检查。本课解释了可以在哪里使用注解、如何应用注解、Java 平台标准版 (Java SE API) 中有哪些预定义的注解类型可用、如何将类型注解与可插拔类型系统结合使用以编写更强大的代码类型检查,以及如何实现重复注解。

原文地址:https://docs.oracle.com/javase/tutorial/java/annotations/index.html


系统注解解析:



什么是元注解:


提取注解相关类:



二:注解处理器概述

       APT即为Annotation Processing Tool,它是javac的一个工具,中文意思为编译时注解处理器,APT可以用来在编译时扫描和处理注解,通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写,注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。

       APT技术被广泛的运用在Java框架中,包括Android项以及Java后台项目,除了上面我们提到的ButterKnife之外,像EventBus 、Dagger2以及阿里的ARouter路由框架等都运用到APT技术,因此要想了解以及探究这些第三方框架的实现原理,APT就是我们必须要掌握的。

       一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这具体的含义什么呢?你可以生成Java代码!这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

       注解处理过程可能会多于一次,官方javadoc定义处理过程:注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入,这些初始输入,可以看成是虚拟的第0批的循环的输出。


三:注解处理器核心类与函数解析

3.1、Processor

3.2、AbstractProcessor

3.3、ProcessingEnvironment

3.4、RoundEnvironment

3.5、Element与Elements接口

3.6、TypeMirror接口


官方中文文档地址:https://tool.oschina.net/apidocs/apidoc?api=jdk-zh


3.1、Processor接口解析

        Processor的每次实现都必须提供一个公共的无参数构造方法,工具将使用该构造方法实例化Processor,工具框架将与实现此接口的类交互,如下所示:

1、如果不使用现有Processor对象,若要创建 Processor实例,则工具将调用Processor类的无参数构造方法。

2、接下来,工具调用具有适当ProcessingEnvironment的init方法。

3、然后,工具调用getSupportedAnnotationTypes、getSupportedOptions和getSupportedSourceVersion,这些方法只在每次运行时调用一次,并非对每个处理轮次调用。

4、在适当的时候,工具在Processor对象上调用process方法,无需为每个处理轮次创建新的Processor对象。


3.2、AbstractProcessor抽象类解析       

AbstractProcessor抽象类是实现了Processor接口,具体类变量和函数解析如下:

       每一个处理器都是继承于AbstractProcessor,如下代码所示:

package com.example;

public class MyProcessor extends AbstractProcessor {

    @Override

    public synchronized void init(ProcessingEnvironment env){ }

    @Override

    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    @Override

    public Set<String> getSupportedAnnotationTypes() { }

    @Override

    public SourceVersion getSupportedSourceVersion() { }

}

解析:

1、init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。

2、process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。

3、getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。

4、getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者。

备注:注解处理器是运行它自己的虚拟机JVM中,javac启动一个完整Java虚拟机来运行注解处理器,这对你意味着什么?你可以使用任何你在其他java应用中使用的的东西,使用guava等。


3.3、ProcessingEnvironment类解析

概述:ProcessingEnvironment注释处理工具框架将提供一个具有实现此接口的对象的注释processor,因此 processor可以使用该框架提供的设施来编写新文件、报告错误消息并查找其他实用工具。

        第三方可能希望提供能包装此接口设施对象的增值包装器,例如,允许多个processor协同写出单个源文件的 Filer扩展。为了实现这一点,对于在其副作用可通过API相互可见的上下文中运行的processor,工具基础设施必须提供相应的设施对象,这些对象是.equals、作为.equals的Filer等等。此外,必须能够配置工具调用,使得从运行注释processor的角度来看,至少已选定的帮助 (helper)类子集可视为由相同的类加载器加载。(因为设施对象管理共享状态,所以包装器类的实现必须知道以前是否包装过相同的基本设施对象。)


3.4、RoundEnvironment类解析

概述:注释处理工具框架将提供一个注释处理器和一个实现此接口的对象,这样处理器可以查询有关注释处理的 round的信息。


3.5、Element接口解析

解析:表示一个程序元素,比如包、类或者方法,每个元素都表示一个静态的语言级构造(不表示虚拟机的运行时构造),元素应该使用equals(Object)方法进行比较,不保证总是使用相同的对象表示某个特定的元素,要实现基于 Element对象类的操作,可以使用visitor或者使用getKind()方法的结果,使用instanceof确定此建模层次结构中某一对象的有效类未必可靠,因为一个实现可以选择让单个对象实现多个Element子接口。

所有已知子接口:

ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。

PackageElement:表示一个包程序元素。提供对有关包及其成员的信息的访问。

TypeElement:表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注释类型是一种接口,TypeElement 表示一个类或接口元素,而 DeclaredType 表示一个类或接口类型,后者将成为前者的一种使用(或调用)。这种区别对于一般的类型是最明显的,对于这些类型,单个元素可以定义一系列完整的类型。例如,元素 java.util.Set 对应于参数化类型 java.util.Set<String> 和 java.util.Set<Number>(以及其他许多类型),还对应于原始类型 java.util.Set。

       此接口每一个都返回元素列表的方法都将按照这些元素在程序信息底层源代码中的自然顺序返回它们。例如,如果信息的底层源代码是 Java 源代码,则按照源代码顺序返回这些元素。

TypeParameterElement:表示一般类、接口、方法或构造方法元素的形式类型参数。类型参数声明一个 TypeVariable。

VariableElement:表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。

        在注解处理过程中,我们扫描所有的Java源文件。源代码的每一个部分都是一个特定类型的Element。换句话说:Element代表程序的元素,例如包、类或者方法。每个Element代表一个静态的、语言级别的构件。在下面的例子中,我们通过注释来说明这个:

解析:你必须换个角度来看源代码,它只是结构化的文本,他不是可运行的,你可以想象它就像你将要去解析的XML文件一样(或者是编译器中抽象的语法树),就像XML解释器一样,有一些类似DOM的元素,你可以从一个元素导航到它的父或者子元素上。

备注:Element代表的是源代码。TypeElement代表的是源代码中的类型元素,例如类。然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror。


3.6、Elements接口解析

用来对程序元素进行操作的实用工具方法,兼容性注意事项:在将来的平台版本中可能会向此接口添加一些方法。


3.7、TypeMirror接口解析

       表示Java编程语言中的类型,这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和null类型,还可以表示通配符类型参数、executable的签名和返回类型,以及对应于包和关键字void的伪类型,应该使用 Types中的实用工具方法比较这些类型,不保证总是使用相同的对象表示某个特定的类型,要实现基于TypeMirror对象类的操作,可以使用visitor或者使用getKind()方法的结果,使用instanceof确定此建模层次结构中某一对象的有效类未必可靠,因为一个实现可以选择让单个对象实现多个TypeMirror子接口。

所有已知子接口:ArrayType、DeclaredType、ErrorType、ExecutableType、NoType、NullType、PrimitiveType、ReferenceType、TypeVariable、WildcardType。


下面汇总一下在编写注解处理器代码时,常常用到的类库,上面已经挑选了比较常用的类深入解析了。


四:如何调试注解处理器

4.1、创建项目模块

4.2、APT注解处理器的调试流程

4.3、注解处理器提示信息


4.1、创建项目模块

       首先我们要创建两个java项目,注意这里是java项目,从名字我们可以看得出来,annotation是自定义的注解模块,而complier注解处理器代码实现,这两个模块如何看过bufferknife、ARouter等一些使用了注解的开源项目中,应该也常常能看到相关的注解相关模块,创建了相关模块后,如果项目app想要使用自定义的注解,我们就需要了解各个模块的项目依赖关系。

依赖关系如下:

1、annotation需要任何依赖。

2、app:

    implementation project(path: ':annotation')

    annotationProcessor project(path: ':compiler')

3、compiler:

    implementation project(path: ':annotation')

    compileOnly 'com.google.auto.service:auto-service:1.0'

    annotationProcessor 'com.google.auto.service:auto-service:1.0'

    implementation 'com.squareup:javapoet:1.13.0'

备注:compiler可以有些比较陌生的依赖项,后面会讲解到。


4.2、APT注解处理器的调试流程

       这里的注解调试流程可以参考本人的另外一篇文章有详细流程:https://www.jianshu.com/p/1ffb85922182(4.3部分)


4.3、注解处理器提示信息

        我们可以通过调用Messager类的函数,实现相关日志的打印。

用例代码如下:

输入日志:(看APT日志并不在Logcat模块下)

备注:注解处理器的错误处理,在init()中,我们也获得了一个Messager对象的引用。Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的。在官方文档中描述了消息的不同级别。非常重要的是Kind.ERROR,因为这种类型的信息用来表示我们的注解处理器处理失败了,让Messager显示相关出错信息,更重要的是注解处理器程序必须完成运行而不崩溃。


五:通过注解处理器动态生成代码

方式一:使用JavaPoet(前身是JavaWriter)

       JavaPoet是square公司的一个开源框架JavaPoet,由Jake Wharton大神所编写。JavaPoet可以用对象的方式来帮助我们生成类代码,也就是我们能只要把要生成的类文件包装成一个对象,JavaPoet便可以自动帮我们生成类文件了。    

JavaPoet的常用类:

JavaFile:控制生成的Java文件的输出的类  

TypeSpec:用于生成类、接口、枚举对象的类

MethodSpec:用于生成方法对象的类

ParameterSpec:用于生成参数对象的类

AnnotationSpec:用于生成注解对象的类

FieldSpec:用于配置生成成员变量的类

ClassName:通过包名和类名生成的对象,在JavaPoet中相当于为其指定Class

ParameterizedTypeName:通过MainClass和IncludeClass生成包含泛型的Class

生成代码实例:


方式二:Filter工具类+StringBuilder实现

代码示例:

总结:

1、通过上面对注解和注解处理器APT的解析,我们可以在此过程中区分出四个角色,帮助我们从更高的角度理解该知识点,四个角色分别为:java项目源码、注解、注解处理器、动态生成的java文件,注解作为桥梁,使注解处理器可以连通java项目源码并解析java项目源码等相关信息,并通过解析出来的信息,动态生成项目需要的java文件。

2、注解处理器的核心工作主要分为两个部分:解析和提取java项目源码和动态生成java项目源码文件。

3、另外,一些项目需要可能需要注解处理器动态的修改java项目源码的代码,这是需要使用ASM技术实现对java项目源码的字节码修改。(参考ARouter项目)


六:问题汇总

问题一:如果要在注解处理中,需要在编译后修改相关的已经存在的Java代码,应该如何实现?

解析:使用ASM技术,实现字节码生成,从而达到在编译后任然可以添加和修改相关java代码,具体实例可以参考开源项目ARouter是如何修改logisticCenter类中的loadRouteMap函数代码。

问题二:注解处理器中autoService的作用是什么?

解析:AutoService注解处理器是Google开发的,用来生成META-INF/services/javax.annotation.processing.Processor文件的。

问题三:有什么开源框架可以很好的学习注解处理器

3.1、https://github.com/alibaba/ARouter

3.2、https://github.com/greenrobot/EventBus

3.3、https://github.com/JakeWharton/butterknife


七:扩展阅读

1、https://blog.csdn.net/qq_20521573/article/details/82321755(Java进阶--编译时注解处理器(APT)详解)

2、https://www.sohu.com/a/190297582_611601(详尽的Android编译时注解处理器教程)

3、https://github.com/square/javapoet(动态生成代码开源库)

4、https://www.jianshu.com/p/acbb293722bc(注解和注解处理器)

5、https://www.cnblogs.com/peida/archive/2013/04/26/3038503.html(深入理解Java:注解(Annotation)--注解处理器)

6、https://tool.oschina.net/apidocs/apidoc?api=jdk-zh(在线文档)

7、https://race604.com/annotation-processing/(国外大神编写,中文文档)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容