Java注解能力提升:教你解析保留策略为源码阶段的注解
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜
在实际开发中,相信你一定写过类似@xxx
的代码,并习惯性的将其放在类和方法上,而这里的 @xxx
在Java
中有一个统一的名称——注解。今天我们便来扒一扒Java
中有关注解的内容,看看其身上究竟藏了哪些我们曾所忽视的信息~
开始之前,不妨先先来看这样一段代码:
@Test
public void annotationTest() {
Class clazz = ExamplePo.class;
MyRequiredArgsConstructor annotation = (MyRequiredArgsConstructor) clazz.getAnnotation(MyRequiredArgsConstructor.class);
if (annotation == null) {
log.info("not Found MyRequiredArgsConstructor annotation");
}else {
log.info(" Found MyRequiredArgsConstructor annotation");
}
}
其中的ExamplePo
及MyRequiredArgsConstructor
如下所示:
@MyRequiredArgsConstructor(includeAllFields = true)
public class ExamplePo {
// ... 省略相关属性信息
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface MyRequiredArgsConstructor {
boolean includeAllFields() default false;
}
笔者
的问题很简单,上述测试代码会输出什么呢?如果你的答案是输出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 @interface MyRequiredArgsConstructor {
boolean includeAllFields() default false;
}
使用javac
命令对MyRequiredArgsConstructor.java
进行编译后生成其对应的MyRequiredArgsConstructor.class
文件。其内容经过反编译后内容如下:
MyRequiredArgsConstructor.class
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) fieldsfirst ansi space
// Source File Name: MyRequiredArgsConstructor.java
package com.example.annotation;
import java.lang.annotation.Annotation;
public interface MyRequiredArgsConstructor
extends Annotation
{
public abstract boolean includeAllFields();
}
不难发现,原先我们在定义注解时使用的@interface
经过编译后被编译为interface
。同时,经过编译器解析后,原先我们定义的 MyRequiredArgsConstructor
还会自动继承了Annotation
这个接口。换言之,注解
的本质就是一个继承了 Annotation
接口。即当我们使用@interface
自定义注解时,其在编译器会自动将@interface
转换为interface
,并自动继承Annotation
。
事实上,在面向对象的思想中接口
通常用于定义一种新的类型
。所以对于Annotation
你完全可以认为其只是一个普通的类型,就像Integer、Short、String
一样属于JDK
的自带的数据类型就可以了。更进一步,在Java
中对于Annotation
这个类型
而言,其主要有如下几点用途:
-
元注解的容器:
Annotation
接口本身也是一个注解,用于定义元注解,即用于注解其他注解的特殊注解,如@Retention
、@Target
等。这为注解的行为和作用域提供了标准化的定义。 -
反射操作: 通过反射机制,可以使用
Annotation
接口的方法获取注解的信息。例如,getAnnotations()
和getAnnotation(Class<T> 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
public class SourceAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(MyRequiredArgsConstructor.class)) {
Name qualifiedName = ((TypeElement) element).getQualifiedName();
Class clazz = null;
try {
clazz = Class.forName(qualifiedName.toString());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
// 获取类名
String className = clazz.getSimpleName();
String packageName = clazz.getPackage().getName();
// 创建构造方法的参数列表
StringBuilder parameters = new StringBuilder();
// 创建构造方法
StringBuilder constructor = new StringBuilder()
.append("public ").append(className).append("Constructor(").append(className).append(" instance) {")
.append(System.lineSeparator());
// 获取类的所有字段
Field[] fields = ReflectUtil.getFields(clazz);
for (Field field : fields) {
String fieldName = field.getName();
// 判断是否包含所有字段
MyRequiredArgsConstructor annotation = AnnotationUtil.getAnnotation(clazz, MyRequiredArgsConstructor.class);
// 获取 includeAllFields 属性值
boolean includeAllFields = annotation != null && annotation.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("Found MyRequiredArgsConstructor on element: " + element);
log.info("generated ExamplePo Construct: \n [{}]",constructor);
}
return true;
}
}
在上述代码中,我们对标有MyRequiredArgsConstructor
注解的类进行了解析,具体来看,对于标有MyRequiredArgsConstructor
注解的类,我们生成其相应final
关键字所修饰字段组成的构造方法。
测试代码
@Test
public void testAnnotationDemo() {
// 伪代码示例,演示如何使用 Compiler API
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(
Arrays.asList(new File("src/test/java/com/example/ExamplePo.java")));
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
task.setProcessors(Arrays.asList(new SourceAnnotationProcessor()));
task.call();
}
输出结果
可以看到我们通过继承AbstractProcessor
类并重写其中process
的逻辑,实现了对MyRequiredArgsConstructor
这一注解的解析。具体来看,通过扫描ExamplePo
上的注解,生成一段其对应的构造方法信息。
事实上,开发者可以编写自定义的注解处理器,继承 AbstractProcessor
并实现 process
方法,而该方法的主要作用用于在编译时对注解进行解析。即:
- 在编译时,编译器会扫描源代码中的注解,并触发相应的注解处理器进行处理。
- 注解处理器的
process
方法中,可以获取到被处理的元素(例如类、方法、字段等)以及它们上的注解信息。
总结
事实上,如果注解的@Retention
标注为SOURCE
,表示该注解仅在源代码中存在,不会被保留到编译后的字节码文件或运行时。在这种情况下,你无法在运行时通过反射直接获取注解信息,因为注解的信息已经在编译时被丢弃。
进一步,如果你需要在运行时获取注解信息,可以将注解的@Retention
标注改为CLASS
或RUNTIME
。如果不修改@Retention
,而又需要在运行时获取注解信息,除了本文提及的通过继承AbstractProcessor
来自定义注解处理器 ,还可以考虑使用字节码操作框架(如 ASM、Byte Buddy
)来修改字节码,将源代码级别的注解信息添加到字节码中。这种方法涉及到对字节码的深度了解,并且需要在类加载时对字节码进行操作!
转载自:https://juejin.cn/post/7345297230200946728