Java 注解处理器及其应用

简介: 前言注解作为一种元数据,需要其他地方进行读取,在前面的文章 重识 Java 注解 中我们了解到,在运行时可以通过反射获取注解信息。元注解 @Retention 定义了注解的保留策略,具体有 SOURCE、CLASS、RUNTIME,那么保留策略不为运行时的注解有什么用呢?

前言

注解作为一种元数据,需要其他地方进行读取,在前面的文章 重识 Java 注解 中我们了解到,在运行时可以通过反射获取注解信息。元注解 @Retention 定义了注解的保留策略,具体有 SOURCE、CLASS、RUNTIME,那么保留策略不为运行时的注解有什么用呢?


除了在运行时获取注解,在编译期其实也可以读取到注解信息。例如保留策略为 SOURCE 的 注解 @Override ,编译器编译源代码时会进行读取,如果发现子类的方法上标注了 @Override ,在父类却不存在对应的方法则会编译失败。


从 JDK 6 开始,Java 提供了注解处理器的一套 API,作为编译器的一个插件,在编译时会执行注解处理器的处理逻辑,以便可以自定义编译时的注解处理逻辑。使用注解处理器的优点便是把运行期处理注解的行为提前到编译器,减少反射的调用,提高程序的性能。


如何使用注解处理器

使用注解处理器有以下步骤。


1、实现 Processor 接口

Processor 是注解处理器的核心接口。实现 Processor 接口的类为自定义的注解处理器,示例如下:


package com.zzuhkp;
import java.util.Set;
import javax.annotation.processing.Completion;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
/**
 * 自定义注解处理器
 *
 * @author zzuhkp
 * @date 2020-08-14 10:50
 * @since 1.0
 */
public class CustomProcessor implements Processor {
    /**
     * 获取注解处理器支持的选项,如在命令行指定的选项
     *
     * @return
     */
    @Override
    public Set<String> getSupportedOptions() {
        return null;
    }
    /**
     * 获取注解处理器支持的注解类型
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return null;
    }
    /**
     * 获取注解处理器支持的源代码版本
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return null;
    }
    /**
     * 注解处理器的初始化方法
     *
     * @param processingEnv
     */
    @Override
    public void init(ProcessingEnvironment processingEnv) {
    }
    /**
     * 处理注解,如果返回true,后面的注解处理器不会继续处理该注解处理器支持的注解
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
    /**
     * 这个确实没看懂
     *
     * @param element
     * @param annotation
     * @param member
     * @param userText
     * @return
     */
    @Override
    public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation,
        ExecutableElement member, String userText) {
        return null;
    }
}


直接实现 Processor 的较为麻烦,Java 提供了一个抽象类 AbstractProcessor 实现了 Processor,我们只需要继承 AbstractProcessor 即可。


public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}


2、注册注解处理器

自定义注解处理器之后,为了让编译器识别,需要进行注册。具体为在类路径中新建文件/META-INF/services/javax.annotation.processing.Processor,文件内容为自定义的注解处理器全限定名,具体和参见下图。这里使用到了 Java 的 SPI 机制,具体可参见前面的文章 Java 基础知识之 SPI。


image.png

注解处理器生命周期

注解处理器相关的 API 只是定义了一些 接口,由使用注解处理器的工具实现,并按照一定的规范调用注解处理器的方法。注解处理器一轮一轮的进行执行,并且可以生成新的 Java 源代码或 class 文件。生成的文件中又可能包含注解处理器需要处理的注解,因此注解处理器可以处理上一轮生成的注解,直到没有需要处理的注解为止。具体生命周期如下。


工具首先实例化注解处理器,因此注解处理器需要提供一个无参数的构造方法。

工具调用 Processor 的 init 方法进行初始化。


工具调用 Processor 的 getSupportedOptions、getSupportedAnnotationTypes、getSupportedSourceVersion 方法获取注解处理器支持的选项、注解类型、源代码版本号,这些方法只会被调用一次。

在随后,工具根据需要调用 Processor 的 process 处理注解处理器支持的注解。


注解处理器相关API

Set getSupportedOptions(): 这个方法用来获取注解处理器支持的选项,随后可以通过相关API获取到在工具启动时指定的支持的选项。


Set getSupportedAnnotationTypes():这个方法用来获取注解处理器支持的注解类型,在继承 AbstractProcessor 时还可以通过在类上添加注解 @SupportedAnnotationTypes 指定。


SourceVersion getSupportedSourceVersion():这个方法用来获取注解处理器支持的源码版本,在继承 AbstractProcessor 时可以在类上添加注解 @SupportedSourceVersion 指定,如果没有指定则默认为 JDK 6。


void init(ProcessingEnvironment processingEnv):注解处理器实例化后的初始化方法。ProcessingEnvironment 表示注解处理器的环境,可以用来获取一些实用的工具类,包含的方法及含义具体如下。


public interface ProcessingEnvironment {
    /**
     * 从工具启动的选项中获取注解处理器支持的选项
     */
    Map<String,String> getOptions();
    /**
     * 返回用来报告错误,警告或其他信息的工具类。
     * 注意:如果Messager 打印了错误类型的消息,将引发错误。
     */
    Messager getMessager();
    /**
     * 获取创建源文件、class文件的工具类。
     */
    Filer getFiler();
    /**
     * 返回获取元素信息的工具类。
     * 元素是对 Java 程序各部分的抽象,如包、类、变量、方法等。
     */
    Elements getElementUtils();
    /**
     * 返回可以获取类型信息的工具类。
     * 类型是指 Java 中的类型,不完全等同于元素,部分元素和类型可以进行转换。
     */
    Types getTypeUtils();
    /**
     * 获取生成的源文件和class应该支持的版本。
     */
    SourceVersion getSourceVersion();
    /**
     * 获取当前区域信息
     */
    Locale getLocale();
}


boolean process(Set annotations, RoundEnvironment roundEnv):注解处理器的核心方法,处理支持的注解,如果返回true则后面的处理器不会再处理方法参数中的注解。RoundEnvironment 表示当前轮的环境,提供了一些实用的方法,具体如下:


public interface RoundEnvironment {
    /**
     * 当前是否为最后一轮处理
     */
    boolean processingOver();
    /**
     * 上一轮处理是否引发错误
     */
    boolean errorRaised();
    /**
     * 获取上一轮注解处理生成的根元素
     */
    Set<? extends Element> getRootElements();
    /**
     * 根据注解获取元素
     */
    Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
    /**
     * 根据注解获取元素
     */
    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}


另外注解处理器还提供了一个方法 Iterable getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText):这个方法确实没看明白,有了解的小伙伴请留言告知,感谢。


Java 注解处理器的应用

笔者了解到 Java 注解处理器有在以下开源项目涉及的场景中进行使用。


属性复制

在项目中,我们常常会将一个类的属性值赋值给另一个对象,常见的做法有三种。


手动创建对象复制,适用于属性较少的情况,如果比较多则会导致代码膨胀。

使用 org.apache.commons.beanutils.BeanUtils#copyProperties,使用反射实现,并添加了属性的校验,性能较差。

使用 org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object),使用反射实现,相对 apache-beanutils 包中的复制属性方法性能较好。

上述是较为常见的做法,还有一种方式是使用开源框架是 mapstruct,它便利用 Java 注解处理器,将运行时通过反射获取属性的行为提前到编译器,生成属性复制的实现方法,提高了性能,具体使用方式可参见 mapstruct 官网。


简化代码

项目中,对于实例类,我们经常需要写 get/set/toString/hashCode 等方法。为了减少这些重复的操作,可以使用 lombok 。lombok 同样使用注解处理器,帮助我们生成这些方法。具体的使用可参考 lombok官网。


总结

本篇首先引出注解处理器的使用时机,然后介绍了如何使用注解处理器及注解处理器的生命周期,最后介绍了注解处理器的 API 及在开源项目中的使用,相信一定还有其他场景让注解处理器大放异彩。


目录
相关文章
|
1天前
|
安全 IDE Java
Java串口通信技术探究2:RXTX库单例测试及应用
Java串口通信技术探究2:RXTX库单例测试及应用
15 4
|
1天前
|
设计模式 存储 前端开发
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
12 4
|
1天前
|
存储 前端开发 安全
13:会话跟踪技术Session的深度应用与实践-Java Web
13:会话跟踪技术Session的深度应用与实践-Java Web
12 3
|
1天前
|
存储 前端开发 搜索推荐
12:会话跟踪技术Cookie的深度应用与实践-Java Web
12:会话跟踪技术Cookie的深度应用与实践-Java Web
12 4
|
1天前
|
XML 存储 Java
11:Servlet中初始化参数的获取与应用-Java Web
11:Servlet中初始化参数的获取与应用-Java Web
13 3
|
2天前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【5月更文挑战第1天】 在移动开发的世界中,性能优化始终是开发者关注的焦点。随着Kotlin的兴起,许多团队和开发者面临着一个选择:是坚持传统的Java语言,还是转向现代化、更加简洁的Kotlin?本文通过深入分析和对比Kotlin与Java在Android应用开发中的性能表现,揭示两者在编译效率、运行速度和内存消耗等方面的差异。我们将探讨如何根据项目需求和团队熟悉度,选择最适合的语言,以确保应用的高性能和流畅体验。
|
3天前
|
安全 Java 程序员
Java并发编程:理解并应用ReentrantLock
【4月更文挑战第30天】 在多线程的世界中,高效且安全地管理共享资源是至关重要的。本文深入探讨了Java中的一种强大同步工具——ReentrantLock。我们将从其设计原理出发,通过实例演示其在解决并发问题中的实际应用,以及如何比传统的synchronized关键字提供更灵活的锁定机制。文章还将讨论在使用ReentrantLock时可能遇到的一些挑战和最佳实践,帮助开发者避免常见陷阱,提高程序性能和稳定性。
|
3天前
|
供应链 Java API
Java 8新特性解析及应用区块链技术在供应链管理中的应用与挑战
【4月更文挑战第30天】本文将深入探讨Java 8的新特性,包括Lambda表达式、Stream API和Optional类等。通过对这些新特性的详细解析和应用实例,帮助读者更好地理解和掌握Java 8的新技术。
|
3天前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【4月更文挑战第30天】在Android开发领域,Kotlin作为一种现代化的编程语言,因其简洁性和功能性受到了开发者的广泛欢迎。尽管与传统的Java相比,Kotlin提供了诸多便利,但关于其性能表现的讨论始终未息。本文将深入分析Kotlin和Java在Android平台上的性能差异,通过实际测试数据揭示两种语言在编译效率、运行速度以及内存占用方面的具体表现,并探讨如何利用Kotlin的优势来提升Android应用的整体性能。
|
3天前
|
SQL 存储 Java
令应用开发效率飙升的 Java 类库
更多地使用 Java 而避免存储过程和复杂 SQL 是当前应用开发的一个潮流,这会在架构上带来优势,但用 Java 实现 SQL 式的运算并不是非常方便,很多任务要从头写起,开发效率其实反而会降低。