Java注解进阶二

本文继续介绍Java编译时注解的相关内容,仍然都是一些偏理论的梳理知识点。主要涉及到几个类,它们都是在处理自定义注解时会常到的,分别是RoundEnvironment、ProcessingEnvironment、Filer、ElementType、ElementKind、TypeKind、TypeMirror。

虽然说是编译时注解,不过在上面几个类的使用上面并不会涉及到编译时什么特殊格式要求,仍然是常规的Java语法,陌生的可能是在API的调用使用上面。

RoundEnvironment

errorRaised()boolean类型的方法,该方法一般很少使用,基本上都是遵循“尽管存在错误,仍保持生成代码”的策略。另外,如果process()方法执行过程中出现错误,并不建议throws一个异常,建议采用直接输出错误信息的方式,类似log信息,后面会介绍到。

processingOver()boolean类型的方法,如果当前轮次不是最后一轮,则processingOver()返回false,否则返回true。在前面介绍过,在执行process()方法时,一般会执行多轮,所以processingOver()方法可以作为判断是否是最后一轮。Butterknife、EventBus以及AutoService中,AutoService有对该方法的典型应用场景,它在多轮编译过程中,只在最后一轮去生成相对应的文件信息,而前面几轮的编译过程中,仅是收集缓存相对应的注解类相关信息。

getElementsAnnotatedWith()方法是一个重载方法,方法的返回值是一个Element类型的Set集合,入参有两种类型,一种是Class类型,这里就是自定义的某种注解类型,一种是TypeElement类型,TypeElement位于javax.lang.model.element包下,是Element的子类型,代表了一种类或者接口类型,下文会对TypeElement做进一步介绍。

RoundEnvironment比较常用方法也就上面几个,在JDK1.9中又新增了两个方法,这里就不再展示了,有兴趣的可以参看网上其它内容。

ProcessingEnvironment

在AbstractProcessor的init()初始化方法中有一个ProcessingEnvironment类型入参。ProcessingEnvironment相当于一个工具类集合,它可以通过该工具类进行写入文件、打印日志以及查找其它元素类型。

Map getOptions()该方法返回的其实是方法Set

getSupportedOptions()或者注解SupportedOptions配置的值。一般getOptions()获取一些debug信息,比如配置一下是否是debug模式,只在debug模式下输出一些日志调试信息。getOptions()方法也比较常用,但是拿到的键值对并不是在代码中赋值好的,IDE工具中都有配置该键值对的选项,可以在运行时灵活配置。

通过getOptions()方法获取选项参数,在gradle文件中配置选项参数值。

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ['verbose' : '1']
            }
        }
    }
}

通过ProcessingEnvironment去获取对应的参数。

processingEnvironment.getOptions().get("verbose");

Messager getMessager()方法返回的是一个Messager类型的对象,这个类型要同Android中Messenger区分开来。Messager提供了注解解释器报告错误信息,警告和其它通知的方法。但它的功能不仅限于Logger,虽然它确实可以在开发过程中充当这个作用。Messager是用于写入消息到使用你的注解解释器的第三方开发者的。在官方文档中有不同层次的信息描述。其中很重要的一个是Kind.ERROR,因为这类消息是用于标示我们的解释器处理失败。

Filer getFiler()方法返回的是一个Filer类型的对象,Filer可以通过注解处理器创建新文件。编译时注解可以创建文件的核心类就是Filer类,可以通过如下方式拿到字符输出流,后面就是标准IO流的操作了。

JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(fileName);          
BufferedWriter writer = new BufferedWriter(sourceFile.openWriter());

在每次运行注解处理工具期间,具有给定路径名的文件只能被创建一次。如果该文件在第一次试图创建它之前就已经存在,则将删除原有的内容。在运行期间,任何试图创建相同文件的后续尝试都将抛出FilerException,试图针对相同的类型名称和相同的包名称创建类文件和源文件也将如此。

Filer

Filer一般可以创建三种类型文件:源文件、类文件和辅助资源文件。

public interface Filer {
    JavaFileObject createSourceFile(CharSequence name,
                                    Element... originatingElements) throws IOException;

    JavaFileObject createClassFile(CharSequence name,
                                   Element... originatingElements) throws IOException;

    FileObject createResource(JavaFileManager.Location location,
                             CharSequence pkg,
                             CharSequence relativeName,
                             Element... originatingElements) throws IOException;

    FileObject getResource(JavaFileManager.Location location,
                           CharSequence pkg,
                           CharSequence relativeName) throws IOException;
}
方法返回值描述
createSourceFileJavaFileObject创建源文件
createClassFileJavaFileObject创建类文件
createResourceFileObject创建辅助资源文件

Element

Element是一个接口类型,代表了一个程序元素,比如包、类或者方法。每个元素都表示一个静态的语言级构造(不表示虚拟机的运行时构造)。

Element是父接口,它包含如下几个子接口。

类型描述
PackageElement包元素。提供对有关包及其成员的信息的访问
ExecutableElement类、接口的方法元素。包括构造方法、注解类型
VariableElement字段、enum、方法、构造方法参数、局部变量、资源变量、异常参数
TypeElement类、接口元素。提供对有关类型及其成员的信息的访问
TypeParameterElement类、接口、方法、构造方法的参数元素

Element有如下几个常用方法。

方法名返回值描述
getKind()ElementKind返回此元素的种类:包、类、接口、方法、字段等
asType()TypeMirror返回元素的数据类型(基本类型、引用类型、数组类型等等)
getSimpleName()Name返回此元素的简单名称,类似Class的getSimpleName()
getEnclosingElement()Element返回此元素父元素
getEnclosedElements()Element返回此元素直接子元素
getModifiersSet<Modifier>返回此元素的权限修饰符
getAnnotation<A extends Annotation> A 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的
getAnnotationMirrorsList<? extends AnnotationMirror> 获该元素上的注解类型。AnnotationMirror表示一个注解,它将注解值与注解类型的每个元素关联。

在注解处理器解析注解元素时,会接触到与Element相关的几个类,它们分别是ElementType、ElementKind、TypeKind、TypeMirror等。如果需要解析出某个Element元素,就需要知道与Element相关几个类的使用场景。

ElementType

ElementType在自定义注解声明时会用到,它是一个enum类型。ElementType与元注解Target一起使用,以指定在什么情况下使用注解类型是合法的。

类型描述
TYPE类、接口(包括注释类型)或枚举声明
FIELD字段声明,有时叫属性声明
METHOD方法声明
PARAMETER参数声明
CONSTRUCTOR构造方法声明
LOCAL_VARIABLE局部变量声明
ANNOTATION_TYPE注解类型声明
PACKAGE包声明
TYPE_PARAMETER类型(包括泛型类型)声明
TYPE_USE使用类型的任意语句中声明

TYPE_PARAMETER与TYPE_USE是从JDK1.8新增的类型注解,TYPE_PARAMETER表示该注解能写在任何类型变量的声明语句中,TYPE_USE表示该注解能写在使用类型的任何语句中(eg:声明语句、泛型和强制转换语句中的类型),主要是方便Java开发者使用类型注解和相关插件(Checker Framework)来检查来在编译期检查运行时的异常。

ElementKind

在使用注解判断Element是哪种元素类型时,一般不使用instanceof方式判断,而是通过Element.getKind()去判断对应的类型。

ElementKind有如下几个类型:

属性描述
PACKAGE
ENUM枚举
CLASS
ANNOTATION_TYPE注解
INTERFACE接口
ENUM_CONSTANT枚举常量
FIELD字段
PARAMETER方法参数
LOCAL_VAR局部变量
METHOD方法
CONSTRUCTOR构造方法
TYPE_PARAMETER类型参数

TypeMirror

在注解处理过程中,可以通过TypeMirror表示Java语言的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和null类型。还可以表示通配符类型参数、executable的签名和返回类型,以及对应于包和关键字void的伪类型。

Element可以通过asType()方法拿到元素TypeMirror。TypeMirror是一个顶层的父接口,如下是部分常使用的子接口。

类型描述
ExecutableTypeExecutable类型(方法、构造方法、初始化)
NoType在实际类型不适合的地方使用的伪类型
PrimitiveType基本数据类型
WildcardType通配符类型参数
ReferenceType引用类型
NullTypeNull类型
DeclaredType声明类型(类或接口类型)
ErrorType异常类或接口类型)
TypeVariable类型变量
ClassType类类型
ArrayType数组类型
PackageType包类型
AnnotatedType注解类型
MethodType方式类型

TypeMirror可以通过getKind()方法获取TypeKind。在上文介绍过如果想要获取某个元素是哪种类型元素,可以通过ElementKind来判断,其实想要判断该元素属于哪种数据类型,也不建议使用instanceof作为判断依据,而应该使用TypeKind。

TypeKind

有时候可能会被ElementKind或者TypeKind的命名混淆,其实可以这样理解,比方说,可以判断通过ElementKind判断某个Element是方法还是类型变量,如果判断出来是类型变量,那么该类型变量是基本数据类型还是引用数据类型,或者说是哪种类型的基本数据类型,这时候就可以使用TypeKind来进行辅助判断。

属性描述
BOOLEAN基本类型booelan
INT基本类型int
LONG基本类型long
FLOAT基本类型float
DOUBLE基本类型double
VOID对应于关键字void的伪类型
NULLnull类型
ARRAY数组类型
PACKAGE对应于包元素的伪类型
EXECUTABLE方法、构造方法、初始化

小结

作为进阶篇的第二篇,介绍的仍然仅是几个处理编译时注解的常用类,还没有涉及到编码的实现,可以说目前为止还没有上正餐。个人观点,虽然是一些基础,但是多少还是需要耗些时间梳理下,为后面的示例展示提供必要的API调用基础。

在下一篇中,也是作为进阶篇的终篇,将会展示一个高仿版的ButterKnife。通过一个仿写版的ButterKnife,作为回答“我们常说的编译时注解,究竟在说什么”,或者说“ButterKnife的实现原理是什么”。

参考资料

关于注解你所需要知道的一切

注解处理,RoundEnvironment.processingOver()

编译期注解学习五 - ElementKind,TypeKind,不同Element类型判断

Java 《注解篇》 编译时注解

Java 8 新特性:扩展注解(类型注解和重复注解)

评论

您确定要删除吗?删除之后不可恢复