当前位置: 首页 > 科技观察

深耕Java注解和注解解析器,架构师一定要知道

时间:2023-03-22 17:33:14 科技观察

什么是元数据(metadata)元数据是由元数据翻译过来的,所谓元数据就是“关于数据的数据”,更通俗的说就是数据描述数据,描述性的关于数据和信息资源的信息。比如一个文本文件,有创建时间、创建者、文件大小等数据,可以理解为元数据。在java中,元数据以标签的形式存在于java代码中,它的存在并不影响程序代码的编译和执行。通常用于生成其他文件或在运行时获知运行代码的描述信息。java中的javadoc和注解都属于元数据。什么是注释?注解是从java5.0开始加入的,可以用来标记包、类、方法、变量等,比如我们常见的@Override,或者Android源码中的@hide、@systemApi、@privateApi等。Override,大多数人往往知道但不知道为什么。今天先说说Annotation背后的秘密,开始正文。元注解是定义注解的注解。Java为我们提供了用于定义注解的基本注解。在java.lang.annotation包中,我们可以看到目前有以下元注解:@Retention@Target@Inherited@Documented@interface接下来我们收集@Override注解来讲解五个基本注解的用法。@interface@interface是java中用于声明注解类的关键字。使用这个注解意味着java.lang.annotation.Annotation类会被自动继承,把这个过程交给编译器。因此如果我们要定义一个注解,我们只需要做下面的事情。以@Override注解为例public@interfaceOverride{}需要注意:在定义注解时,不能继承其他注解或接口@Retention@Retention:该注解用于定义注解保留策略,即定义的时候注释类存在(源代码阶段或编译或运行阶段之后)。该注解接受如下参数:RetentionPolicy.SOURCE、RetentionPolicy.CLASS、RetentionPolicy.RUNTIME,其具体用途和含义如下:看一下@Override注解的保留策略:@Retention(RetentionPolicy.SOURCE)public@interfaceOverride{}表示@Override注解只存在于源码阶段,javac在编译时去掉该注解。@Target这个注解用来定义注解的目标,也就是可以在什么地方使用注解,比如是用在方法上还是用在字段上,注解接受以下参数:以@Override举个例子,不难看出它的目标是一个方法:@Target(ElementType.METHOD)public@interfaceOverride{}到现在为止,通过@interface,@Retention,@Target可以完全定义一个注解。我们来看看@Override的完整定义:@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public@interfaceOverride{}@Inherited默认情况下,我们在父类上使用的自定义注解不会被子类继承。如果想让子类也继承父类的注解,即注解在子类中也会生效。自定义注解时需要设置@Inherited。一般情况下,这个注解用的比较少。@Documented该注解用于描述其他类型的注解,这些注解应该被javadoc记录并出现在apidoc中。例如@Target使用这个注解会出现在api描述中@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public@interfaceTarget{ElementType[]value();}借助@Interface,@Target,@Retention,@Inherited,@Documented这五个元注解,我们可以自定义注解,前三个注解是任何注解所必需的。自定义注解格式:public@interface注解名{定义体}定义体是方法的集合,每个方法实际上声明了一个配置参数。方法的名称作为配置参数的名称,方法的返回值类型为配置参数的类型。与普通方法不同,default关键字可以用来声明默认的配置参数Value。注意:这里只能使用public或者defaultdefault两种权限修饰符。配置参数的类型只能使用基本类型(byte,boolean,char,short,int,long,float,double)和String,Enum,Class,annotation对于只包含一个配置参数的注解,参数名推荐为设置在value中,即方法名是value。一旦设置了配置参数,其参数值就必须有一个确定的值,要么在使用注解时指定,要么在定义注解时,使用default来设置设置默认值。对于非基本类型的参数值,不能为空。和@Override一样,没有成员定义的注解称为标签注解。我们已经学习了如何在注解处理器之上定义注解。要使注解发挥实际作用,我们需要为注解编写相应的注解处理器。根据注解的特点,注解处理器可以分为运行时注解处理和编译时注解处理器。运行时处理器需要借助反射机制来实现,而编译时处理器需要借助APT来实现。无论是运行时注解处理器还是编译时注解处理器,主要的工作就是读取注解和处理具体的注解。从这个角度看,注解处理器还是很容易理解的。注解处理器(AnnotationProcessor)是javac的一个工具,用于在编译时扫描编译和处理注解(Annotation)。你可以自己定义注解和注解处理器来做一些事情。一种注解处理器,它将Java代码或(编译后的字节码)作为输入并生成文件(通常是java文件)。这些生成的java文件不能修改,会像他们手工编写的java代码一样被javac编译。看到这里再加上前面的理解,应该明白大概的流程了,就是把标注了注解的类和变量作为输入内容,通过注解处理器进行处理,生成你想要生成的java代码。Runtimeannotationprocessor(不推荐)熟悉java反射机制的同学一定对java.lang.reflect包非常熟悉。该包中的所有API都支持读取runtimeAnnotation的能力,即属性为@Retention(RetentionPolicy.RUNTIME)。java.lang.reflect中的AnnotatedElement接口是所有程序元素的(Class,Method)父接口。我们可以通过反射获取某个类的AnnotatedElement对象,然后使用该对象提供的方法获取注解信息的常用方法如下:写一个运行时注解处理器的本质就是通过反射获取注解信息,然后执行其他操作。编译运行时注解处理器就这么简单。运行时注解通常用于参数配置模块。编译时注解处理器不同于运行时注解处理器,编译时注解处理器(AnnotationProcessorTool)。APT用于在编译时扫描和处理注释信息。具体的annotationprocessor可以是java源文件或者编译后的最终class文件作为输入,然后输出其他文件,可以是.java文件也可以是.class文件,但通常我们输出的都是.java文件。(注意:它不是对源文件的修改)。如果输出的是一个.java文件,而这些.java文件是由javac和其他源代码文件一起编译出来的。您可能想知道,注解处理器在什么阶段进行干预?嗯,其实是在javac开始编译之前,这也是通常我们愿意输出.java文件的原因。Annotations最早在java5引入,主要包括apt和com.sum.mirror包中的相关镜像API。这时候apt和javac是独立的。从java6开始,注解处理器正式标准化,apt工具也直接集成到javac中。让我们回到如何编写编译时注解处理器的话题。编译一个编译时注解处理主要分为两步:1.继承AbstractProcessor,实现自己的注解处理器2.注册处理器,先打包成jar包。看下一个标准的注解处理器的格式:publicclassMyAnnotationProcessorextendsAbstractProcessor{@OverridepublicSetgetSupportedAnnotationTypes(){returnsuper.getSupportedAnnotationTypes();}@OverridepublicSourceVersiongetSupportedSourceVersion(){returnsuper.getSupportedSourceVersion();}@Overridepublicsynchronizedvoidinit(ProcessingEnvironmentprocessingEnv){super.init(processingEnv);}@Overridepublicbooleanprocess(Setannotations,RoundEnvironmentroundEnv){returnfalse;}}要编写注解处理器,您必须首先非常熟悉ProcessingEnvironment和RoundEnvironment。接下来我们来看看这两个类的风格。首先我们看一下ProcessingEnvironment类:publicinterfaceProcessingEnvironment{MapgetOptions();//Messager用于报错、警告等提示MessagegergetMessager();//Filter用于新建源文件,class文件和辅助文件FilergetFiler();//Elements包含操作Element的工具方法ElementsgetElementUtils();//Types包含操作TypeMirror的工具方法TypesgetTypeUtils();SourceVersiongetSourceVersion();LocalegetLocale();}Elementelement表示一个静态,语言级组件。而任何结构化文档都可以看作是一个由不同元素组成的结构,比如XML、JSON等。对于java源文件,Element代表程序元素:包、类、方法都是程序元素,也是一个结构化的元素document:packagecom.closedevice;//PackageElementpublicclassMain{//TypeElementprivateintx;//VariableElementprivateMain(){//ExecuteableElement}privatevoidprint(Stringmsg){//参数部分Stringmsg为TypeElement}}我们还需要关注TypeMirror的三种类型:DeclaredType表示声明的类型:类类型或接口类型,当然也包括参数化类型,例如Set还包括原始类型TypeElement表示类或接口元素,DeclaredType表示类类型或接口类型。TypeMirror表示java语言中的类型。类型包括基本类型、声明类型(类类型和接口类型)、数组、类型变量和空类型。还表示通配符类型参数、签名和可执行文件的返回类型等。TypeMirror类中最重要的是getKind()方法,它返回TypeKind类型。为了您的方便,这里是源代码:publicenumTypeKind{BOOLEAN,BYTE,SHORT,INT,LONG,CHAR,FLOAT,DOUBLE,VOID,NONE,NULL,ARRAY,DECLARED,ERROR,TYPEVAR,WILDCARD,PACKAGE,EXECUTABLE,OTHER,UNION,INTERSECTION;publicbooleanisPrimitive(){switch(this){caseBOOLEAN:caseBYTE:caseSHORT:caseINT:caseLONG:caseCHAR:caseFLOAT:caseDOUBLE:returntrue;default:returnfalse;}}}简单的说,Element代表源码,TypeElement代表源码中的类型元素,比如一个类。虽然我们可以从TypeElement中得到类名,但是TypeElement并不包含类本身的信息,比如它的父类。要获取这些信息,我们需要用到TypeMirror,我们可以通过Element中的asType()获取元素对应的TypeMirror。然后再看看RoundEnvironment,这个类比较简单,慢慢来:publicinterfaceRoundEnvironment{booleanprocessingOver();//上一轮注解处理器是否产生错误booleanerrorRaised();//返回注解处理器产生的根元素最后一轮注解处理器SetgetRootElements();//返回包含指定注解类型的元素集合SetgetElementsAnnotatedWith(TypeElementa);//返回包含指定注解类型的元素集合SetgetElementsAnnotatedWith(Classa);}FilerFiler用于在注释处理器中创建新文件。由于Filer使用起来比较麻烦,后面我们会使用javapoet来简化我们的操作。打包注释处理器时需要一个特殊文件javax.annotation.processing。Processor在META-INF/services路径下新建工程://javapoet代码生成框架实现'com.squareup:javapoet:1.8.0'//注解处理器实现'com.google.auto.service:auto-service:1.0-rc6'annotationProcessor'com.google.auto.service:auto-service:1.0-rc6'编译时注解demo示例地址:https://gitee.com/yutg/apt.git项目结构--apt-demo----bindview-annotation(JavaLibrary)//注解定义----bindview-api(AndroidLibrary)//定义SDK接口方法----bindview-compiler(JavaLibrary)//注解处理器相关操作并生成java文件---应用程序(安卓应用程序)