Java注解能力提升:教你解析保留策略为源码阶段的注解
?思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航??
?在实际开发中,相信你一定写过类似@xxx的代码,并习惯性的将其放在类和方法上,而这里的 @xxx在Java中有一个统一的名称——「注解」。今天我们便来扒一扒Java中有关注解的内容,看看其身上究竟藏了哪些我们曾所忽视的信息~
开始之前,不妨先先来看这样一段代码:
@Test
publicvoidannotationTest(){
Classclazz=ExamplePo.class;
MyRequiredArgsConstructorannotation=(MyRequiredArgsConstructor)clazz.getAnnotation(MyRequiredArgsConstructor.class);
if(annotation==null){
log.info("notFoundMyRequiredArgsConstructorannotation");
}else{
log.info("FoundMyRequiredArgsConstructorannotation");
}
}
其中的ExamplePo及MyRequiredArgsConstructor如下所示:
@MyRequiredArgsConstructor(includeAllFields=true)
publicclassExamplePo{
//...省略相关属性信息
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public@interfaceMyRequiredArgsConstructor{
booleanincludeAllFields()defaultfalse;
}
笔者的问题很简单,上述测试代码会输出什么呢?如果你的答案是输出log.info("not Found MyRequiredArgsConstructor annotation");那说明你对于注解掌握的还算可以。
在此基础上,如果我接着追问你有办法解析RetentionPolicy.SOURCE的注解吗?感到束手无策也别慌,相信读完今天的文章你一定会有所收获的。
究竟什么是注解我们知道在Java中的注释通常通过//来进行标识,依靠注释我们可以很快了解代码的大致逻辑。那有没一种手段,可以让编译器快速理解我们的代码呢?答案便是我们今天所谈论的注解。
在 Java 中,注解(Annotation)主要为程序提供额外信息。「通常注解可以用于类、方法、字段、参数等元素上,以提供有关这些元素的描述信息,而这些信息可以在编译时或运行时可以被其他程序读取和利用。」
你可能觉得这样的描述略带晦涩,为了方便理解,你完全可以将注解类比于标签,它可以贴在一个类、一个方法或者字段上。这样的做的目的就是为了告知编译器在编译时特别注意,进而执行某些特定的操作信息。
注解的本质「虽然注解我们平时都在用,但你是否考虑过注解的本质到底是什么呢?」 其是一个class?还是一个interface?亦或是一种全新的类型呢?
为了解开这一疑惑,我们决定自己定义了一个名为@MyRequiredArgsConstructor的注解,然后将其编译为.class文件,进而依靠反编译.class来查看注解经Java编译器后的产物究竟是什么。
?MyRequiredArgsConstructor.java注解
?@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public@interfaceMyRequiredArgsConstructor{
booleanincludeAllFields()defaultfalse;
}
使用javac命令对MyRequiredArgsConstructor.java进行编译后生成其对应的MyRequiredArgsConstructor.class文件。其内容经过反编译后内容如下:
?MyRequiredArgsConstructor.class
?//DecompiledbyJadv1.5.8e2.Copyright2001PavelKouznetsov.
//Jadhomepage:http://kpdus.tripod.com/jad.html
//Decompileroptions:packimports(3)fieldsfirstansispace
//SourceFileName:MyRequiredArgsConstructor.java
packagecom.example.annotation;
importjava.lang.annotation.Annotation;
publicinterfaceMyRequiredArgsConstructor
extendsAnnotation
{
publicabstractbooleanincludeAllFields();
}
不难发现,原先我们在定义注解时使用的@interface经过编译后被编译为interface。同时,经过编译器解析后,原先我们定义的MyRequiredArgsConstructor还会自动继承了Annotation这个接口。「换言之,注解的本质就是一个继承了 Annotation 接口。即当我们使用@interface自定义注解时,其在编译器会自动将@interface转换为interface,并自动继承Annotation。」
事实上,在面向对象的思想中接口通常用于定义一种新的类型。所以对于Annotation你完全可以认为其只是一个普通的类型,就像Integer、Short、String一样属于JDK的自带的数据类型就可以了。更进一步,在Java中对于Annotation这个类型而言,其主要有如下几点用途:
「元注解的容器:」 Annotation 接口本身也是一个注解,用于定义元注解,即用于注解其他注解的特殊注解,如 @Retention、@Target 等。这为注解的行为和作用域提供了标准化的定义。
「反射操作:」 通过反射机制,可以使用 Annotation 接口的方法获取注解的信息。例如,getAnnotations() 和 getAnnotation(Class annotationClass) 方法允许在运行时获取类、方法、字段等上的注解实例,便于在程序中动态处理和检查注解。
「处理注解的工具类:」 Java提供了一些工具类(如 AnnotationUtils),这些工具类中的方法接受 Annotation 接口的实例,提供了方便的方式来处理和操作注解。
明白了注解的本质就是一个类型为Annotation的接口后,接下来我们再来看与注解相关的一些细节问题。
注解的细节正如前文所述,注解本质上是一种注释或标记,所以其主要用于提供额外的信息,进而使得代码更容易被编译器所阅读和理解。既然注解可以视为一种注释,那么其主要功能便在于提供更直观的代码解释。
我们知道,对于以 // 表示的注释而言,主要的受众是相关的开发者;但对于注解而言,其主要受众是编译器。「换句话说,如果编译器没有对注解进行相应的解析和处理,那么注解的存在就变得毫无意义。」 因此,注解在代码中的价值在于它与编译器的协作。
而解析一个类或者方法的注解的时机通常会有两种,一种是编译期直接的扫描,一种是运行期反射。而作用于编译器时的注解最常用的便是 @Override。即如果某个类中的方法被 @Override所修饰,那么编译器在编译期间就会检查当前方法的方法签名是否真正重写了父类的某个方法,并比较父类中是否具有一个同样的方法签名。
进一步,为了更好的区分注解的解析时机,在Java内部会通过元注解@Retention来定义注解的保留策略,即:
RetentionPolicy.SOURCE:注解仅在源代码阶段保留,编译时会被丢弃。RetentionPolicy.CLASS:注解在编译时被保留,但在运行时会被丢弃。RetentionPolicy.RUNTIME:注解在运行时被保留,可以通过反射获取。更进一步来看,正如我们之前所说对于注解的理解其实可以理解为便签。但这个便签可不是随处都可以张贴的,其会"张贴"的位置会受到的@Target这一元注解的限制,而@Target所支持的范围具体如下所示:
ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上ElementType.FIELD:允许作用在属性字段上ElementType.METHOD:允许作用在方法上ElementType.PARAMETER:允许作用在方法参数上ElementType.CONSTRUCTOR:允许作用在构造器上ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上ElementType.ANNOTATION_TYPE:允许作用在注解上ElementType.PACKAGE:允许作用在包上例如我们之前定义的MyRequiredArgsConstructor注解其在使用中可以放置在类、接口和接口上。
事实上,除了我们这里谈及的@Retention、@Target外,JDK中还有一些其他的元注解信息,例如
「@Documented:」
用于指定被该注解修饰的注解类将被 javadoc 工具提取成文档。「@Inherited:」
用于指定被注解的类的子类是否也继承该注解。如果一个类使用了 @Inherited 修饰的注解,其子类在没有显式声明该注解的情况下也会继承该注解。(ps:对于元注解而言,其实是一种特殊的注解,主要用于注解其他注解)
这些元注解为注解的定义和使用提供了更高层次的控制和灵活性。通过使用元注解,开发者可以规定注解的生命周期、作用范围、文档生成等方面的行为。这使得注解能够更好地适应各种场景和需求。
解析SOURCE策略的注解经过之前的分析,我们知道由于MyRequiredArgsConstructor注解的@Retention标注为SOURCE因此其表示该注解仅在源代码中存在,而不会被保留到编译后的字节码文件或运行时。因此在这种情况下,我们无法在运行时通过反射直接获取注解信息,因为注解的信息已经在编译时被丢弃。那么有一种方式读取@Retention标注为SOURCE的注解呢?当然是有的,笔者这里提供一种继承AbstractProcessor的方式,具体代码如下:
@SupportedAnnotationTypes("com.example.annotation.MyRequiredArgsConstructor")
@Slf4j
publicclassSourceAnnotationProcessorextendsAbstractProcessor{
@Override
publicbooleanprocess(Set?extendsTypeElementannotations,RoundEnvironmentroundEnv){
for(Elementelement:roundEnv.getElementsAnnotatedWith(MyRequiredArgsConstructor.class)){
NamequalifiedName=((TypeElement)element).getQualifiedName();
Classclazz=null;
try{
clazz=Class.forName(qualifiedName.toString());
}catch(ClassNotFoundExceptione){
thrownewRuntimeException(e);
}
//获取类名
StringclassName=clazz.getSimpleName();
StringpackageName=clazz.getPackage().getName();
//创建构造方法的参数列表
StringBuilderparameters=newStringBuilder();
//创建构造方法
StringBuilderconstructor=newStringBuilder()
.append("public").append(className).append("Constructor(").append(className).append("instance){")
.append(System.lineSeparator());
//获取类的所有字段
Field[]fields=ReflectUtil.getFields(clazz);
for(Fieldfield:fields){
StringfieldName=field.getName();
//判断是否包含所有字段
MyRequiredArgsConstructorannotation=AnnotationUtil.getAnnotation(clazz,MyRequiredArgsConstructor.class);
//获取includeAllFields属性值
booleanincludeAllFields=annotation!=nullannotation.includeAllFields();
if(includeAllFields||Modifier.isFinal(field.getModifiers())){
//生成构造方法代码
parameters.append(fieldName).append(",");
constructor.append("this.").append(fieldName).append("=instance.").append(fieldName).append(";\n");
}
}
//删除末尾的逗号和空格
if(parameters.length()0){
parameters.setLength(parameters.length()-2);
}
//完成构造方法
constructor.append("}");
//处理MySourceAnnotation注解,可以在此处获取注解信息
log.info("FoundMyRequiredArgsConstructoronelement:"+element);
log.info("generatedExamplePoConstruct:\n[{}]",constructor);
}
returntrue;
}
}
在上述代码中,我们对标有MyRequiredArgsConstructor注解的类进行了解析,具体来看,对于标有MyRequiredArgsConstructor注解的类,我们生成其相应final关键字所修饰字段组成的构造方法。
?测试代码
?@Test
publicvoidtestAnnotationDemo(){
//伪代码示例,演示如何使用CompilerAPI
JavaCompilercompiler=ToolProvider.getSystemJavaCompiler();
StandardJavaFileManagerfileManager=compiler.getStandardFileManager(null,null,null);
Iterable?extendsJavaFileObjectcompilationUnits=fileManager.getJavaFileObjectsFromFiles(
Arrays.asList(newFile("src/test/java/com/example/ExamplePo.java")));
JavaCompiler.CompilationTasktask=compiler.getTask(null,fileManager,null,null,null,compilationUnits);
task.setProcessors(Arrays.asList(newSourceAnnotationProcessor()));
task.call();
}
?输出结果
?image.png可以看到我们通过继承AbstractProcessor 类并重写其中process的逻辑,实现了对MyRequiredArgsConstructor这一注解的解析。具体来看,通过扫描ExamplePo上的注解,生成一段其对应的构造方法信息。
事实上,开发者可以编写自定义的注解处理器,继承 AbstractProcessor 并实现 process 方法,而该方法的主要作用用于在编译时对注解进行解析。即:
在编译时,编译器会扫描源代码中的注解,并触发相应的注解处理器进行处理。注解处理器的 process 方法中,可以获取到被处理的元素(例如类、方法、字段等)以及它们上的注解信息。总结事实上,如果注解的@Retention标注为SOURCE,表示该注解仅在源代码中存在,不会被保留到编译后的字节码文件或运行时。在这种情况下,你无法在运行时通过反射直接获取注解信息,因为注解的信息已经在编译时被丢弃。
进一步,如果你需要在运行时获取注解信息,可以将注解的@Retention标注改为CLASS或RUNTIME。如果不修改@Retention,而又需要在运行时获取注解信息,除了本文提及的通过继承AbstractProcessor来「自定义注解处理器」 ,还可以考虑使用字节码操作框架(如 ASM、Byte Buddy)来修改字节码,将源代码级别的注解信息添加到字节码中。这种方法涉及到对字节码的深度了解,并且需要在类加载时对字节码进行操作!
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线