Lombok原理探析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: ## 前言对于一个Java开发者来说,Lombok应该是使用最多的插件之一了,他提供了一系列注解来帮助我们减轻对重复代码的编写,例如实体类中大量的setter,getter方法,各种IO流等资源的关闭、try…catch…finally模版等,虽然可以通过IDE的快捷帮我们生成这些方法,但这些冗长的代码仍会影响代码的简洁性与可阅读性。如今,随着使用者数量越来越多,Lombok甚至成为IDEA的

前言

对于一个Java开发者来说,Lombok应该是使用最多的插件之一了,他提供了一系列注解来帮助我们减轻对重复代码的编写,例如实体类中大量的setter,getter方法,各种IO流等资源的关闭、try…catch…finally模版等,虽然可以通过IDE的快捷帮我们生成这些方法,但这些冗长的代码仍会影响代码的简洁性与可阅读性。
如今,随着使用者数量越来越多,Lombok甚至成为IDEA的内置插件了(2020.3 版本+),可见其影响力。
20201209172523435.jpg
但不知道使用Lombok的你,是否思考过这种自动生成代码究竟是什么原理呢?这些代码又是怎么产生的呢?而这就是本文要展开介绍的内容了。

Lombok的安装与使用网络上相关的介绍已经很多了,这里就不多说了,自行查阅相关资料即可。

注解解析的两种方式

关于注解,我在之前的文章里有过详细的介绍,在解释Lombok的原理之前,推荐你先阅读《给编译器看的注释——「注解」》
这里主要回顾一下Lombok注解的解析方式。

运行时解析

这是最常见的注解解析的方式,运行时能够解析的注解,必须将@Retention设置为RUNTIME,这样就可以通过反射拿到该注解。
例如使用最多的@RequestMapping注解就是一个运行时注解。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";
    
    @AliasFor("path")
    String[] value() default {};
    
    @AliasFor("value")
    String[] path() default {};
    
    RequestMethod[] method() default {};
    
    String[] params() default {};
    
    String[] headers() default {};
    
    String[] consumes() default {};
    
    String[] produces() default {};
}

运行时注解的解析原理也很简单,在java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。相关的例子在之前的文章中有介绍过,这里不赘述了。
那Lombok的注解也是这种原理吗?
翻开源码,我们可以看到@Data这个接口RetentionPolicy是SOURCE级别的,也就是说,在代码编译的时候,相关的注解信息就已经丢掉了,并不会被加载进JVM里,那么为什么我们又会在Compile代码的时候看见那些get/set方法呢?这就不得不说另一种注解解析方式了——编译时解析

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
    String staticConstructor() default "";
}

编译时解析

Javac 编译器的编译过程大致可分为 1个准备过程3个处理过程 :

  1. 初始化插入式注解处理器
  2. 解析与填充符号表;
  3. 插入式注解处理器的注解处理;
  4. 分析与字节码生成。

这 3 个步骤之间的关系如下图所示:
image.png
而其中编译期的注解处理有两种机制,分别简单描述下:

Annotation Processing Tool(APT)

APT自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,APT被替换主要有2点原因:

  • 相关API都在com.sun.mirror非标准包下
  • 没有集成到javac中,需要额外运行

Pluggable Annotation Processing API(插件式注解处理器)

image.png
Java6开始纳入了JSR-269规范:Pluggable Annotation Processing API(插件式注解处理器)。作为APT的替代方案,JSR-269提供一套标准API来处理Annotations,并解决了刚才提到的APT的两个问题。
在使用javac的过程中,它产生作用的具体流程如下所示
image.png

  1. javac对源代码进行分析,生成了一棵抽象语法树(AST)
  2. 运行过程中调用实现了「JSR 269 API」的Lombok程序
  3. 此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
  4. javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)
AST是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。

想要实现一个基于「JSR 269 API」的程序也很容易具体来说,我们只需要继承AbstractProcessor类,重写process()方法实现自己的注解处理逻辑,并且在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor,在javac编译过程中编译器便会调用我们实现的Annotation Processor,从而使得我们有机会对java编译过程中生产的抽象语法树进行修改。

对于Java编译的一些补充

对于Java的编译期,其实是一个相对模糊的概念,需要针对具体的情况具体分析。

  1. *.java文件转为 *.class的过程称为编译器的前端(前端编译)。例如:JDK的javac编译器。
  2. 把字节码( *.class文件) 转变为 本地机器码 的过程称为Java虚拟机的即时编译运行期(JIT编译器,Just In Time)。例如:HotSpot虚拟机的C1、C2编译器。
  3. 使用静态的提前编译器(AOT编译器,Ahead Of Time Compiler)直接把程序变异成与目标及其指令集相关的二进制代码的过程。例如:JDK的Jaotc。

他们之间的关系大约是
15734544331781.jpg
javac把 *.java文件编译成*.class文件,*.class文件进入JVM后,通过JIT编译器将*.class文件解释为对应的机器码。而AOT则是直接把*.class文件编译系统的库文件,不再依靠JIT去做这个事情。

Javac 这类编译器对代码的 运行效率几乎没有任何优化措施,但由于该阶段离程序员编码是最近的(相较于JIT而言),所以对于程序员编码来说,前端编译器在编译期的优化更加密切,许多新生的 Java 语法特性,都是靠编译器的 「语法糖」(自动装箱、拆箱与遍历循环)来实现的,这是因为 Javac 做了很多针对 Java 语言编码过程的优化措施来改善程序员的编码风格、提升编码效率,而不是依赖虚拟机的底层改进来支持。
相关文章
|
19天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
38 5
|
29天前
|
存储 算法 Java
Java HashSet:底层工作原理与实现机制
本文介绍了Java中HashSet的工作原理,包括其基于HashMap实现的底层机制。通过示例代码展示了HashSet如何添加元素,并解析了add方法的具体过程,包括计算hash值、处理碰撞及扩容机制。
|
8天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
9天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
11天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
2月前
|
算法 Java
JAVA并发编程系列(8)CountDownLatch核心原理
面试中的编程题目“模拟拼团”,我们通过使用CountDownLatch来实现多线程条件下的拼团逻辑。此外,深入解析了CountDownLatch的核心原理及其内部实现机制,特别是`await()`方法的具体工作流程。通过详细分析源码与内部结构,帮助读者更好地理解并发编程的关键概念。
|
17天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
34 2
|
20天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
17天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
31 1
|
23天前
|
存储 安全 Java
深入理解Java中的FutureTask:用法和原理
【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。