探索Java日志的奥秘:底层日志系统-log4j2

2019 年 4 月 11 日 云栖社区


前言


log4j2是apache在log4j的基础上,参考logback架构实现的一套新的日志系统(我感觉是apache害怕logback了)。


log4j2的官方文档上写着一些它的优点:


  • 在拥有全部logback特性的情况下,还修复了一些隐藏问题

  • API 分离:现在log4j2也是门面模式使用日志,默认的日志实现是log4j2,当然你也可以用logback(应该没有人会这么做)

  • 性能提升:log4j2包含下一代基于LMAX Disruptor library的异步logger,在多线程场景下,拥有18倍于log4j和logback的性能

  • 多API支持:log4j2提供

    Log4j 1.2, SLF4J, Commons Logging and java.util.logging (JUL) 的API支持

  • 避免锁定:使用Log4j2 API的应用程序始终可以选择使用任何符合SLF4J的库作为log4j-to-slf4j适配器的记录器实现

  • 自动重新加载配置:与Logback一样,Log4j 2可以在修改时自动重新加载其配置。与Logback不同,它会在重新配置发生时不会丢失日志事件。

  • 高级过滤: 与Logback一样,Log4j 2支持基于Log事件中的上下文数据,标记,正则表达式和其他组件进行过滤。

  • 插件架构: Log4j使用插件模式配置组件。因此,您无需编写代码来创建和配置Appender,Layout,Pattern Converter等。Log4j自动识别插件并在配置引用它们时使用它们。

  • 属性支持:您可以在配置中引用属性,Log4j将直接替换它们,或者Log4j将它们传递给将动态解析它们的底层组件。

  • Java 8 Lambda支持

  • 自定义日志级别

  • 产生垃圾少:在稳态日志记录期间,Log4j 2 在独立应用程序中是无垃圾的,在Web应用程序中是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应时间性能。

  • 和应用server集成:版本2.10.0引入了一个模块log4j-appserver,以改进与Apache Tomcat和Eclipse Jetty的集成。


Log4j2类图:



这次从四个地方去探索源码:启动,配置,异步,插件化


源码探索


启动


log4j2的关键组件


  • LogManager


根据配置指定LogContexFactory,初始化对应的LoggerContext


  • LoggerContext


1、解析配置文件,解析为对应的java对象。
2、通过LoggerRegisty缓存Logger配置
3、Configuration配置信息
4、start方法解析配置文件,转化为对应的java对象
5、通过getLogger获取logger对象


  • Logger


LogManaer


该组件是Log4J启动的入口,后续的LoggerContext以及Logger都是通过调用LogManager的静态方法获得。我们可以使用下面的代码获取Logger


Logger logger = LogManager.getLogger();


可以看出LogManager是十分关键的组件,因此在这个小节中我们详细分析LogManager的启动流程。


LogManager启动的入口是下面的static代码块:



这段静态代码段主要分为下面的几个步骤:


  1. 首先根据特定配置文件的配置信息获取loggerContextFactory

  2. 如果没有找到对应的Factory的实现类则通过ProviderUtil中的getProviders()方法载入providers,随后通过provider的loadLoggerContextFactory方法载入LoggerContextFactory的实现类

  3. 如果provider中没有获取到LoggerContextFactory的实现类或provider为空,则使用SimpleLoggerContextFactory作为LoggerContextFactory。


根据配置文件载入LoggerContextFactory



在这段逻辑中,LogManager优先通过配置文件”log4j2.component.properties”通过配置项”log4j2.loggerContextFactory”来获取LoggerContextFactory,如果用户做了对应的配置,通过newCheckedInstanceOf方法实例化LoggerContextFactory的对象,最终的实现方式为:



在默认情况下,不存在初始的默认配置文件log4j2.component.properties,因此需要从其他途径获取LoggerContextFactory。


通过Provider实例化LoggerContextFactory对象


代码:



这里比较有意思的是hasProviders和getProviders都会通过线程安全的方式去懒加载ProviderUtil这个对象。跟进lazyInit方法:



再看构造方法:



这里的懒加载其实就是懒加载Provider对象。在创建新的providerUtil实例的过程中就会直接实例化provider对象,其过程是先通过getClassLoaders方法获取provider的类加载器,然后通过loadProviders(classLoader);加载类。在providerUtil实例化的最后,会统一查找”META-INF/log4j-provider.properties”文件中对应的provider的url,会考虑从远程加载provider。而loadProviders方法就是在ProviderUtil的PROVIDERS列表中添加对一个的provider。可以看到默认的provider是org.apache.logging.log4j.core.impl.Log4jContextFactory



很有意思的是这里懒加载加上了锁,而且使用的是
lockInterruptibly这个方法。lockInterruptibly和lock的区别如下:


lock 与 lockInterruptibly比较区别在于:


lock 优先考虑获取锁,待获取锁成功后,才响应中断。


lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。


ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的。


Thread.interrupt 方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。 ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。


上面有一句注释值得注意:



原来这里是为了让osgi可以阻止启动。


再回到logManager:


可以看到在加载完Provider之后,会做factory的绑定:



到这里,logmanager的启动流程就结束了。


配置


在不使用slf4j的情况下,我们获取logger的方式是这样的:


Logger logger = logManager.getLogger(xx.class)


跟进getLogger方法:



这里有一个getContext方法,跟进,



上文提到factory的具体实现是Log4jContextFactory,跟进getContext


方法:



直接看start:



发现其中的核心方法是reconfigure方法,继续跟进:



可以看到每一个configuration都是从ConfigurationFactory拿出来的,我们先看看这个类的getInstance看看:



这里可以看到ConfigurationFactory中利用了PluginManager来进行初始化,PluginManager会将ConfigurationFactory的子类加载进来,默认使用的XmlConfigurationFactory,

JsonConfigurationFactory,YamlConfigurationFactory这三个子类,这里插件化加载暂时按下不表。


回到reconfigure这个方法,我们看到获取ConfigurationFactory实例之后会去调用getConfiguration方法:



跟进getConfiguration,这里值得注意的是有很多个getConfiguration,注意甄别,如果不确定的话可以通过debug的方式来确定。



这里就会根据之前加载进来的factory进行配置的获取,具体的不再解析。


回到reconfigure,之后的步骤就是setConfiguration,入参就是刚才获取的config



这个方法最重要的步骤就是config.start,这才是真正做配置解析的



这里面有如下步骤:


  1. 获取日志等级的插件

  2. 初始化

  3. 初始化Advertiser

  4. 配置


先看一下初始化,也就是setup这个方法,setup是一个需要被复写的方法,我们以XMLConfiguration作为例子,



发现这里面有一个比较重要的方法constructHierarchy,跟进:



发现这个就是一个树遍历的过程。诚然,配置文件是以xml的形式给出的,xml的结构就是一个树形结构。回到start方法,跟进doConfiguration:



发现就是对刚刚获取的configuration进行解析,然后塞进正确的地方。回到start方法,可以看到昨晚配置之后就是开启logger和appender了。


异步


AsyncAppender



log4j2突出于其他日志的优势,异步日志实现。我们先从日志打印看进去。找到Logger,随便找一个log日志的方法。



一路跟进



可以看出这个在打日志之前做了调用次数的记录。跟进tryLogMessage,



继续跟进:



这里可以看到在实际打日志的时候,会从config中获取打日志的策略,跟踪ReliabilityStrategy的创建,发现默认的实现类为DefaultReliabilityStrategy,跟进看实际打日志的方法



这里实际打日志的方法居然是交给一个config去实现的。。。感觉有点奇怪。。跟进看看



可以清楚的看到try之前是在创建LogEvent,try里面做的才是真正的log(好tm累),一路跟进。



接下来就是callAppender了,我们直接开始看AsyncAppender的append方法:



这里主要的步骤就是:


  1. 生成logEvent

  2. 将logEvent放入BlockingQueue,就是transfer方法

  3. 如果BlockingQueue满了则启用相应的策略


同样的,这里也有一个线程用来做异步消费的事情



直接看run方法:

  1. 阻塞获取logEvent

  2. 将logEvent分发出去

  3. 如果线程要退出了,将blockingQueue里面的event消费完在退出。


AsyncLogger



直接从AsyncLogger的logMessage看进去:



跟进logWithThreadLocalTranslator,



这里的逻辑很简单,就是将日志相关的信息转换成RingBufferLogEvent(RingBuffer是Disruptor的无所队列),然后将其发布到RingBuffer中。发布到RingBuffer中,那肯定也有消费逻辑。这时候有两种方式可以找到这个消费的逻辑。


  • 找disruptor被使用的地方,然后查看,但是这样做会很容易迷惑

  • 按照Log4j2的尿性,这种Logger都有对应的start方法,我们可以从start方法入手寻找


在start方法中,我们找到了一段代码:



直接看看这个RingBufferLogEventHandler的实现:



顺着接口找上去,发现一个接口:



通过注释可以发现,这个onEvent就是处理逻辑,回到RingBufferLogEventHandler的onEvent方法,发现里面有一个execute方法,跟进:



这个方法就是实际打日志了,AsyncLogger看起来还是比较简单的,只是使用了一个Disruptor。


插件化


之前在很多代码里面都可以看到



其实整个log4j2为了获得更好的扩展性,将自己的很多组件都做成了插件,然后在配置的时候去加载plugin。


跟进collectPlugins。



处理逻辑如下:


  1. 从Log4j2Plugin.dat中加载所有的内置的plugin

  2. 然后将OSGi Bundles中的Log4j2Plugin.dat中的plugin加载进来

  3. 再加载传入的package路径中的plugin

  4. 最后加载配置中的plugin


逻辑还是比较简单的,但是我在看源码的时候发现了一个很有意思的东西,就是在加载log4j2 core插件的时候,也就是


PluginRegistry.getInstance().loadFromMainClassLoader()


这个方法,跟进到decodeCacheFiles:



可以发现加载时候是从一个文件(PLUGIN_CACHE_FILE)获取所有要获取的plugin。看到这里的时候我有一个疑惑就是,为什么不用反射的方式直接去扫描,而是要从文件中加载进来,而且文件是写死的,很不容易扩展啊。然后我找了一下PLUGIN_CACHE_FILE这个静态变量的用处,发现了PluginProcessor这个类,这里用到了注解处理器。



(不太重要的方法省略)


我们可以看到在process方法中,PluginProcessor会先收集所有的Plugin,然后在写入文件。这样做的好处就是可以省去反射时候的开销。


然后我又看了一下Plugin这个注解,发现它的RetentionPolicy是RUNTIME,一般来说PluginProcessor是搭配

RetentionPolicy.SOURCE,CLASS使用的,而且既然你把自己的Plugin扫描之后写在文件中了,RetentionPolicy就没有必要是RUNTIME了吧,这个是一个很奇怪的地方。


小结


总算是把Log4j2的代码看完了,发现它的设计理念很值得借鉴,为了灵活性,所有的东西都设计成插件式。互联网技术日益发展,各种中间件层出不穷,而作为工程师的我们更需要做的是去思考代码与代码之间的关系,毫无疑问的是,解耦是最具有美感的关系。


更多精彩


如何准备阿里技术面试?终面官现身说法!


对话Ruby创始人松本行弘、阿里高级技术专家朴灵!


【资料合集】2019阿里云峰会•北京回顾合集:PDF下载

如果觉得本文还不错,点击好看一下!
点击阅读原文进入免费申请小程序云应用!
登录查看更多
1

相关内容

SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志System
【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
192+阅读 · 2020年6月29日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
FPGA加速系统开发工具设计:综述与实践
专知会员服务
65+阅读 · 2020年6月24日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【初学者系列】tensorboard学习笔记
专知
7+阅读 · 2019年10月4日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
支持多标签页的Windows终端:Fluent 终端
Python程序员
7+阅读 · 2019年4月15日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
VrR-VG: Refocusing Visually-Relevant Relationships
Arxiv
6+阅读 · 2019年8月26日
Arxiv
6+阅读 · 2018年2月7日
VIP会员
相关资讯
【初学者系列】tensorboard学习笔记
专知
7+阅读 · 2019年10月4日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
支持多标签页的Windows终端:Fluent 终端
Python程序员
7+阅读 · 2019年4月15日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
Top
微信扫码咨询专知VIP会员