Java注解能力提升:教你解析保留策略为源码阶段的注解


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


在实际开发中,相信你一定写过类似@xxx的代码,并习惯性的将其放在类和方法上,而这里的 @xxxJava中有一个统一的名称------注解 。今天我们便来扒一扒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");
    }
}

其中的ExamplePoMyRequiredArgsConstructor如下所示:

java 复制代码
@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注解

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface MyRequiredArgsConstructor {

    boolean includeAllFields() default false;
}

使用javac命令对MyRequiredArgsConstructor.java进行编译后生成其对应的MyRequiredArgsConstructor.class文件。其内容经过反编译后内容如下:

MyRequiredArgsConstructor.class

java 复制代码
// 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这个类型而言,其主要有如下几点用途:

  1. 元注解的容器: Annotation 接口本身也是一个注解,用于定义元注解,即用于注解其他注解的特殊注解,如 @Retention@Target 等。这为注解的行为和作用域提供了标准化的定义。

  2. 反射操作: 通过反射机制,可以使用 Annotation 接口的方法获取注解的信息。例如,getAnnotations()getAnnotation(Class<T> annotationClass) 方法允许在运行时获取类、方法、字段等上的注解实例,便于在程序中动态处理和检查注解。

  3. 处理注解的工具类: 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中还有一些其他的元注解信息,例如

  1. @Documented:

    • 用于指定被该注解修饰的注解类将被 javadoc 工具提取成文档。
  2. @Inherited:

    • 用于指定被注解的类的子类是否也继承该注解。如果一个类使用了 @Inherited 修饰的注解,其子类在没有显式声明该注解的情况下也会继承该注解。

(ps:对于元注解而言,其实是一种特殊的注解,主要用于注解其他注解)

这些元注解为注解的定义和使用提供了更高层次的控制和灵活性。通过使用元注解,开发者可以规定注解的生命周期、作用范围、文档生成等方面的行为。这使得注解能够更好地适应各种场景和需求。

解析SOURCE策略的注解

经过之前的分析,我们知道由于MyRequiredArgsConstructor注解的@Retention标注为SOURCE因此其表示该注解仅在源代码中存在,而不会被保留到编译后的字节码文件或运行时。因此在这种情况下,我们无法在运行时通过反射直接获取注解信息,因为注解的信息已经在编译时被丢弃。那么有一种方式读取@Retention标注为SOURCE的注解呢?当然是有的,笔者这里提供一种继承AbstractProcessor的方式,具体代码如下:

java 复制代码
@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关键字所修饰字段组成的构造方法。

测试代码

java 复制代码
@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 方法,而该方法的主要作用用于在编译时对注解进行解析。即:

  1. 在编译时,编译器会扫描源代码中的注解,并触发相应的注解处理器进行处理。
  2. 注解处理器的 process 方法中,可以获取到被处理的元素(例如类、方法、字段等)以及它们上的注解信息。

总结

事实上,如果注解的@Retention标注为SOURCE,表示该注解仅在源代码中存在,不会被保留到编译后的字节码文件或运行时。在这种情况下,你无法在运行时通过反射直接获取注解信息,因为注解的信息已经在编译时被丢弃。

进一步,如果你需要在运行时获取注解信息,可以将注解的@Retention标注改为CLASSRUNTIME。如果不修改@Retention,而又需要在运行时获取注解信息,除了本文提及的通过继承AbstractProcessor自定义注解处理器 ,还可以考虑使用字节码操作框架(如 ASM、Byte Buddy)来修改字节码,将源代码级别的注解信息添加到字节码中。这种方法涉及到对字节码的深度了解,并且需要在类加载时对字节码进行操作!

相关推荐
IT学长编程1 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码1 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries2 小时前
读《show your work》的一点感悟
后端
郭二哈2 小时前
C++——模板进阶、继承
java·服务器·c++
A尘埃2 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23072 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
沉登c2 小时前
幂等性接口实现
java·rpc
Marst Code2 小时前
(Django)初步使用
后端·python·django
代码之光_19802 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端