likes
comments
collection
share

第一性原理看 SpringBoot 日志设计

作者站长头像
站长
· 阅读数 33

大家好,我是摸鱼总工,读源码是程序员的美德,快乐才能成为总工。

前两天,因为一个 SpringBoot 日志配置未生效的坑,写了一篇文章专门讲解排坑的过程。写完之后,觉得不过瘾,为了排坑都看了不少源码了,咋能不升个维度,总结一下设计原理呢?

读源码第一性原理

作为工(mo)作(yu)多年的老司机,我读源码,有一个习惯,就是喜欢先假设这个东西不存在,然后从 0 开始,想出一个大致的设计。最后,拿着这个设计,去跟源码对照。这样不仅可以加快读源码的速度,也能真正学到东西,还能体会到快乐。

从设计去猜测源码,然后从源码反推设计,在宏观和微观之间来回穿梭,真正做到融会贯通。这也是我认为的读源码第一性原理,不可省略,不可违反。

说了这么多,我们来实践一下吧!

从 0 开始设计

现在开始,我们假设自���是 SpringBoot 的设计师,然后需要提供一套日志方案给开发者使用。

接过设计任务,第一件事情,自然就是寻找已有的解决方案。如果已有的方案非常成熟,那我们直接集成进来就完事了。如果已有的方案有缺陷,那恭喜你,创新的机会也来了。

在 Java 世界,日志系统当然是有比较成熟的方案的,那就是大名鼎鼎的 slf4j,它提供了一套统一的API,同时提供 log4j 和 logback 等成熟的实现方案供你选择。大多数的开源框架,基本都是采用这套方案了。

那这套方案有缺陷吗?

别人的缺陷正好是你创新的机会

我们使用一个组件,首先接触的是 API。没错,API 是非常重要的交互,懂得怎么使用 API,是掌握一个组件的开始。 那除了 API 呢? 最重要的恐怕就是配置了!不同的配置,会直接影响 API 的效果。

偏偏日志系统,API 非常简单,而配置却很复杂。它不仅配置项多,且生效路径也比较隐蔽。比如 Logback,可配置 console、文件、异步、滚动等丰富的选项。其配置文件的生效路径是 “-Dlogback.configurationFile”, “logback-test.xml”, “logback.xml”,遇到大型系统时,如果遇到一些隐秘的冲突问题,排查起来也是非常费劲的!最后,不同的日志实现,比如 log4j 和 logback,是完全不同的配置方式!

大名鼎鼎的 slf4j,解决了 API 的问题,却没有解决配置问题!这是一个遗憾!但也给了 SpringBoot 一个创新的机会。

统一的日志配置体系

从前面的分析可知,SpringBoot 的日志体系,首先要解决的痛点就是统一配置问题。

问题明确了,那我们可以咔咔开始干了。模仿 logback,以 Spring 的风格,设计一套配置方式,类似于下面这样的:

logging.level.root=INFO
logging.level.com.yourpackage=DEBUG

# Logging pattern configuration
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n

# Log file name
logging.file.name=logs/springboot.log

这一步没有问题,接下来是问题是,这样的配置,如何生效呢?

一个简单的方案自然就是,把这些配置,翻译成 logback 配置,然后注入 logback 系统中。

同样的方式,针对 log4j,再写一套翻译器,就可以了。

这个方法简单有效,SpringBoot 也确实就是这么干的!

具体来讲,这个任务就是由 SpringBoot 的 LoggingSystem 来实现的,针对 logback,其实现是

LogbackLoggingSystem,针对 log4j 2,其实现是 Log4J2LoggingSystem。

到了这一步,我相信你大概对整个日志设计思路有一个清晰的了解了。

然而,在实现层面,问题没有这么简单,创新都是有代价的!

兼容与冲突

对旧有的组件进行优化,往往都有兼容与冲突的历史包袱,有时候这些现实问题才是真正的大头。

slf4j 的 API 塑造很成功,而且已经被大量的软件在使用了,修改这个 API 不现实,只能原封不动地保留。

logback 和 log4j 2的配置内容和配置路径,虽然各异,可能给新手造成困扰,但很多老手这一套已经用得很熟练了,不对其进行支持,恐怕会遭其侧目。因此,SpringBoot 日志体系也会支持指定配置文件,以适配原生的配置方式。

这是最基本的兼容性问题了。

更麻烦的问题是冲突!

如果原生配置和 SpringBoot 新增的配置,同时存在,要如何处理呢?

slf4j 有自己的配置加载逻辑,而且是静态的,SpringBoot 日志作为后来者,自然无法对其禁止。不过,好消息是,加载了就加载了吧,我可以对你进行重置啊!重置逻辑,那是要费一番代码的,需要对logback和log4j 2的实现非常了解才可。不过呢,这一套代码,毕竟是对用户不可见,算是恶心自己,成全开发者了。

而更重要的问题是,何时重置呢?

优雅的重置时机

我们知道,slf4j 在第一次调用 LoggerFactory.getLogger 时,会触发配置加载逻辑。

我们也知道,SpringBoot 的核心是一个依赖注入与管理系统,它会按照Bean对象的依赖关系,逐次加载各个对象。

从需求上来说,我们要保证 LoggingSystem 在其它 Bean 对象初始化之前生效,否则这些对象的初始化相关的日志就可能会丢失。

但问题是,我们日常使用日志,一般是不会主动使用到 LoggingSystem 这种东西的。也就是说,我们没办法通过简单的配置方式,把 LoggingSystem 配置为系统的第一依赖。

那 SpringBoot 如何优雅地解决这个问题的呢?

答案是监听器机制。

SpringBoot 支持一个叫 ApplicationListener 的东西,它允许你在应用启动的各个阶段注入一些逻辑。而日志系统就是实现一个 LoggingApplicationListener,然后在 Prepared 阶段,完成日志的配置。

这个实现的优雅之处在于,无需破坏业务依赖管理原则,就可以实现自定义的系统层面的逻辑。

对照源码

到这里,我们按照自己的理解,从 0 开始,推演了一套 SpringBoot 日志设计。对其中的关键问题,都给予了阐述。

那么 SpringBoot 真的是这样设计的吗?其重置逻辑,是全部删除,还是在旧有的配置上进行修改呢?

这些细节的问题,那当然是要对照源码来看了。

你可以跑个 Demo,以 logback 为例,重点关注:

  • org.springframework.boot.context.logging.LoggingApplicationListener
  • org.springframework.boot.logging.logback.LogbackLoggingSystem (logback 的 LoggingSystem 实现)
  • ch.qos.logback.classic.Logger (logback的Logger实现)
  • ch.qos.logback.classic.LoggerContext (logback 的ILoggerFactory实现)

这个工具非常适合对照源码,从源码反推设计。

螺旋式进步

读源码第一性原理,从 0 开始,想出一个大致的设计。从设计猜测源码,从源码验证设计。

但在实践过程中,这个过程可不是一次性来回。

等到你真正开始读源码,你就会开始遇到一些,我们之前没有考虑到的问题。

比如 SpringBoot 日志体系,一旦你开始看源码,就会遇到以下问题:

  • LoggingGroup 是干啥的?为啥要这个玩意儿?
  • fireOnLevelChange 居然能动态修改日志级别?我能主动控制修改不?

最开始的设计,往往细节没有那么丰富,对于关键问题的方案,与原作者也不一定完全一样。

但解决这个不一样的过程,就是真正学习的过程。多次重复这个过程,从不一样到达一样,那你就与原作者融为一体了,才算是真正吃透这个源码了!

愿大家都能快乐读源码,早日成总工!

转载自:https://juejin.cn/post/7382891667673382962
评论
请登录