对于服务端运行的程序来说,日志是极为重要的,无论是日常运行状态的监控还是问题发生后的定位排查,都离不开日志。但在 java 语言中,有着许许多多不同的日志框架供我们使用。
通常,我们会习惯于使用其中的一两种,比如时下最流行的 log4j2 或是 java 原生的 java.util.logging。但在实际的开发中,我们经常会去依赖大量别人提供的 jar 包,我们不能去限制和要求给我们提供 jar 包的人会使用什么日志框架,这样一来,我们就必须学习每一种日志框架的配置方法,并且一一进行配置,否则我们就无法控制项目实际依赖的外部 jar 包中代码的日志行为了,这无疑是过于艰巨的任务,那么,有什么办法可以解决这个难题吗?slf4j 项目就是为此而生的。
slf4j 是 simple logging facade for java 的缩写,可以翻译为 java 的简单日志外观。如上所述,slf4j 这个开源项目的目的就是为我们提供一个一致的 API 来使用不同的日志框架,通过将不同的日志框架桥接到我们熟悉的日志框架上,我们就可以实现用一套配置适配所有日志框架的目的了。
从 slf4j 的名称,以及上图,我们可以看出来,这是一个 facade 设计模式的实现,它通过提供一个日志抽象层,来实现对所有日志框架用法的桥接。
在实际使用中,我们只要依赖 slf4j 的相关依赖,然后通过 LoggerFactory 获取 logger 对象,即可用这个 logger 对象进行相关的日志打印,例如:
public class Demo { public static void main(String[] args) { LoggerFactory.getLogger(Demo.class).error("+++++++"); } }
slf4j 的体系结构如图所示:
图中,我们可以看到,整个体系分为 5 层:
得益于 slf4j 体系结构分层的清晰,了解了上述五层,我想不用再做过多讲解,slf4j 的工作原理已经十分清楚了。
在实际的使用中,我们常常会遇到一些问题,例如虽然配置了 slf4j 却没有按照预期以同一个日志框架的方式输出日志,日志仍然是出现在了多个地方,或者因为一系列包冲突导致项目无法启动,等等问题。这往往是对 slf4j 体系内的 jar 包依赖使用不当导致的。
根据 slf4j 的上述原理,桥接层的各个 jar 包实现了 java 各个日志框架的入口类,因此,我们要使用 slf4j 来桥接所有日志框架的话,就没有必要再保留遗留层的 jar 包了。
同时,由于我们最终要输出到某个我们熟悉并且配置好的目标日志框架上,所以适配器层和实现层应该只保留这个目标日志框架对应的那一套 jar 包,而绝不能依赖两套及以上,否则 slf4j 无法找到日志的输出方式,必然造成包冲突等问题。
既然我们要用目标日志框架来输出日志,我们自然要排除对应框架的桥接包,否则一边适配,又一边桥接,这就陷入到死循环中了。
总结起来,slf4j 使用的三个原则就是:
上述 slf4j 使用原则中有一个问题,那就是图中 inner-java 对应的 java.util.logging 包名下的日志框架是定义在 rt.jar 中的,我们不能排除这个框架包依赖,而由于双亲委派原则,我们也不能通过类加载机制覆盖这个包中的任何类,这样一来,slf4j 就无法实现用 jul-to-slf4j 实现对 jul 日志框架的桥接了。
这是 slf4j 使用中的一个常见的问题,你会发现虽然配置好了 slf4j 的依赖并且正常启动,但基于 jul 的日志仍然输出到了默认的位置,那么,如何来解决这个问题呢?
slf4j 的桥接组件 jul-to-slf4j 与其他桥接组件是完全不同的,它并没有实现它所桥接的 jul 的任何类,而是基于 jul 的框架机制,实现了一套日志输出 handler 来实现日志输出时调用 slf4j-api 的目的。这在官方文档中有详细的介绍:
http://www.slf4j.org/legacy.html
有两种方法可以实现对 jul 的桥接:
在配置文件 ${JDK_HOME}/jre/lib/logging.properties 中配置:
handlers = org.slf4j.bridge.SLF4JBridgeHandler
第二种方法,在程序启动时执行下面的代码,先删除所有的 handler,再添加 slf4j 的 handler:
SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install();