思考,输出,沉淀。用浅显的语言陈说技能,让自己和他人都有所收获。
作者:毅航


在实践开发中,相信你一定写过类似@xxx的代码,并习惯性的将其放在类和办法上,而这儿的 @xxxJava中有一个统一的名称——注解。今天咱们便来扒一扒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如下所示:

@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这个类型而言,其主要有如下几点用处:

  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的办法,详细代码如下:

@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();
}

输出成果

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

能够看到咱们经过承继AbstractProcessor 类并重写其间process的逻辑,实现了对MyRequiredArgsConstructor这一注解的解析。详细来看,经过扫描ExamplePo上的注解,生成一段其对应的结构办法信息。

事实上,开发者能够编写自界说的注解处理器,承继 AbstractProcessor 并实现 process 办法,而该办法的主要效果用于在编译时对注解进行解析。即:

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

总结

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

进一步,假如你需要在运行时获取注解信息,能够将注解的@Retention标示改为CLASSRUNTIME。假如不修正@Retention,而又需要在运行时获取注解信息,除了本文提及的经过承继AbstractProcessor自界说注解处理器 ,还能够考虑运用字节码操作结构(如 ASM、Byte Buddy)来修正字节码,将源代码级别的注解信息添加到字节码中。这种办法涉及到对字节码的深度了解,而且需要在类加载时对字节码进行操作!