把 Springboot 默认的依赖拿掉,然后引入 Log4j2 的包。
这个时候项目依赖图是这样的,可以看到没有 logback-core 了,只有 log4j-core:
再次运行项目,日志实现就变成了 Log4j:
你发现了吗,我除了动了一下 pom 依赖外,其他的代码一行都没有动,日志框架就从 logback 变化为了 log4j。
而且 class 文件没有任何变化,所以我也就不去截图了。
这就是 Slf4j 的功劳,这就是“门面”的含义,这就是为什么都建议大家在项目中使用 Slf4j,而不是具体的诸如 logback、log4j 这样具体的日志实现。
接下来说一下 @Log4j2 这个注解。
我们还是把依赖恢复到最开始纯净的状态,也就是这样:
然后我们把注解修改为 @Log4j2,但是我们项目中这个时候并没有引入 Log4j-core 包,那么你觉得会有问题吗?
不会有问题的,我们可以看一下。
先看一下输出:
此时的日志实现类是 SLF4JLogger。
这玩意哪里来的?
看一下 class 文件:
这个两个类是来自于 log4j-api 包里面,同时由于 log4j-to-slf4j 包的存在,所以最后的实现类桥接到了 SLF4JLogger 中去:
如果我把 log4j-api 包移除掉,你说会不会编译不过呢?
肯定编译不过的,因为包都不存在了,搞不出来 class 文件:
如果我不想用 SLF4JLogger 这个类呢,我就想用真正的 log4j。
简单,把 log4j 的依赖搞进来:
好,我前面说了这么多的废话,不厌其烦的给你排除、引入日志相关的包,给你看输出啥的,而且整个过程中并不涉及到 Lombok 包的变化,都是为了再次印证这两句话:
如果你使用任何 Lombok 的注解,比如 @Log4j,Lombok 将生成使用这些库的代码,但是你的项目里面必须要包含对这些库的依赖,否则 Lombok 生成的代码将无法编译。
比如我前面把 log4j-api 包移除掉了,是不是编译就没有过?
同样地,你要负责在你的运行时中拥有这些包,否则类的初始化可能会失败。
比如我前面把 logback-core 的包移除了,编译的时候没有问题,但是服务运行的时候,是不是抛出找不到类的异常?
是不是再次证明:
聊聊原理
前面我提到了一句“编译时注解”,不知道大家对于这个玩意了不了解。
不了解其实也很正常,因为我们写业务代码的时候很少自定义编译时注解,顶天了搞个运行时注解就差不多了。
Lombok 的核心工作原理就是编译时注解。
其实我了解的也不算深入,只是大概知道它的工作原理是什么样的,对于源码没有深入研究。
但是我可以给你分享一下两个需要注意的地方和可以去哪里了解这个玩意。
首先第一个需要注意的地方是这里:
log 相关注解的源码位于这个部分,可以看到很奇怪啊,这些文件是以 SCL.lombok 结尾的,这是什么玩意?
这是 lombok 的小心思,其实这些都是 class 文件,但是为了避免污染用户项目,它做了特殊处理。
所以你打开这类文件的时候选择以 class 文件的形式打开就行了,就可以看到里面的具体内容。
比如你可以看看这个文件:
lombok.core.handlers.LoggingFramework
你会发现你们就像是枚举似的,写了很多日志的实现:
这个里面把每个注解需要生成的 log 都硬编码好了。正是因为这样,Lombok 才知道你用什么日志注解,应该给你生成什么样的 log。
比如 log4j 是这样的:
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(TargetType.class);
这也同时可以和我们前面的 class 文件对应起来:
而 SLF4J 是这样的:
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TargetType.class);
第二个需要注意的地方是找到入口:
这些 class 文件加载的入口在于这个地方,是基于 Java 的 SPI 机制:
AnnotationProcessorHider 这个类里面有两行静态内部类,我们看其中一个, AnnotationProcessor ,它是继承自 AbstractProcessor 抽象类:
javax.annotation.processing.AbstractProcessor
这个抽象类,就是入口中的入口,核心中的核心。
在这个入口里面,初始化了一个类加载器,叫做 ShadowClassLoader:
它干的事儿就是加载那些被标记为 SCL.lombok 的 class 文件。
然后我是怎么知道 Lombok 是基于编译时注解的呢?
其实这玩意在我看过的两本书里面都有写,有点模糊的印象,写文章的时候我又翻出来读了一遍。
首先是《深入理解 Java 虚拟机(第三版)》的第四部分程序编译与代码优化的第 10 章:前端编译与优化一节。
里面专门有一小节,说插入式注解的:
Lombok 的主要工作地盘,就在 javac 编译的过程中。
在书中的 361 页,提到了编译过程的几个阶段。
从Java代码的总体结构来看,编译过程大致可以分为一个准备过程和三个处理过程:
- 1.准备过程:初始化插入式注解处理器。
- 2.解析与填充符号表过程,包括:
- 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
- 填充符号表。产生符号地址和符号信息。
- 3.插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段,本章的实战部分会设计一个插入式注解处理器来影响Javac的编译行为。
- 4.分析与字节码生成过程,包括:
- 标注检查。对语法的静态信息进行检查。
- 数据流及控制流分析。对程序动态运行过程进行检查。
- 解语法糖。将简化代码编写的语法糖还原为原有的形式。(java中的语法糖包括泛型、变长参数、自动装拆箱、遍历循环foreach等,JVM运行时并不支持这些语法,所以在编译阶段需要还原。)
- 字节码生成。将前面各个步骤所生成的信息转换成字节码。
如果说 javac 编译的过程就是 Lombok 的工作地盘,那么其中的“插入式注解处理器的注解处理过程”就是它的工位了。
书中也提到了 Lombok 的工作原理:
第二本书是《深入理解 JVM 字节码》,在它的第 8 章,也详细的描述了插件化注解的处理原理,其中也提到了 Lombok:
如果你看懂了书中的前面的十几页的描述,那么看这个图就会比较清晰了。
总之,Lombok 的核心原理就是在编译期对于 class 文件的魔改,帮你生成了很多代码。
这也是作者提到的:
invisible source code,看不见的源码。
这里的看不见,指的是 java 文件中的看不见,在 class 文件中它还是无处遁形。
如果你有兴趣深入了解它的原理的话,可以去看看我前面提到的这两本书,里面都有手把手的实践开发。
我就不写了,一个原因是因为确实门槛较高,写出来生涩难懂。另外一个原因那不是因为我懒嘛。
本文已经收录至个人博客,欢迎大家来玩: