本篇文章中涉及到的所有代码都已经上传到gitee中: gitee.com/sss123a/log…
java日志系列全解
# 03.java日志之log4j的基础组件和各种Appender
# 07.java日志之logback内部状态数据Status
# 09.java日志之logback的appender标签及其子标签全解析
slf4j-api.2.0.10
| 日志 API 和配置分离 | 鉴于 SLF4J 提供了一个狭窄的 API,仅限于编写日志语句,但没有日志配置,SLF4J 强制执行关注点分离。日志记录语句是使用 SLF4j API 编写的,并通过底层日志记录后端进行配置,通常在单个位置。 |
| 在部署时选择您的日志框架 | 通过在类路径上插入适当的 jar 文件(提供程序/绑定),可以在部署时插入所需的日志记录框架。 |
| 快速失败操作 | 在 SLF4J 初始化期间,将很早就搜索提供者。如果 SLF4J 在类路径上找不到提供程序,它将发出一条警告消息并默认为无操作实现。 |
| 流行日志框架的提供者 | SLF4J 支持流行的日志框架,即 reload4j、log4j 1.x、log4j 2.x、java.util.logging、Simplelogging 和 NOP。logback 、logevents、penna项目 原生支持 SLF4J。 |
| 桥接旧版日志记录 API | JCL 在 SLF4J 上的实现(即 jcl-over-slf4j.jar)将允许您的项目逐步迁移到 SLF4J,而不会破坏与使用 JCL 的现有软件的兼容性。同样,log4j-over-slf4j.jar 和 jul-to-slf4j 模块将允许您将 log4j 和 java.util.logging 调用分别重定向到 SLF4J。有关更多详细信息,请参阅桥接旧版 API页面。 |
| 迁移您的源代码 | slf4j-migrator实用程序可以帮助您迁移源以使用 SLF4J。 |
| 支持参数化日志消息 | 所有 SLF4J 提供程序/绑定都支持参数化日志消息,并显着提高了性能 结果。 |
slf4j官网介绍
Simple Logging Facade for Java (SLF4J) 用作各种日志框架(例如 java.util.logging、logback、log4j)的简单外观或抽象,允许最终用户在部署时插入所需的日志框架 。
在您采用SLF4J之前,我们建议您阅读简明的SLF4J用户手册。
请注意,启用 SLF4J 库意味着仅添加一个强制依赖项,即slf4j-api.jar。如果在类路径上找不到binding/provider,则 SLF4J 将默认为无操作实现。
如果您希望将 Java 源文件迁移到 SLF4J,请考虑我们的迁移器工具,它可以在短短几分钟内将您的项目迁移为使用 SLF4J API。
如果您依赖的外部维护组件使用 SLF4J 以外的日志记录 API,例如 commons logging、log4j 或 java.util.logging,请查看 SLF4J 对旧版 API的二进制支持。
以上内容翻译自:slf4j官方网站
这里面提到的binding/provider其实是slf4j中实现日志门面的两个重要class,
binding对应org.slf4j.spi.LoggerFactoryBinder(2.x版本中已经过期了)
package org.slf4j.spi;
import org.slf4j.ILoggerFactory;
public interface LoggerFactoryBinder {
ILoggerFactory getLoggerFactory();
String getLoggerFactoryClassStr();
}
provider对应org.slf4j.spi.SLF4JServiceProvider
package org.slf4j.spi;
import org.slf4j.ILoggerFactory;
import org.slf4j.IMarkerFactory;
public interface SLF4JServiceProvider {
ILoggerFactory getLoggerFactory();
IMarkerFactory getMarkerFactory();
MDCAdapter getMDCAdapter();
String getRequestedApiVersion();
void initialize();
}
SLF4JServiceProvider类在被反射实例化后,调用initialize()方法进行初始化
我们可以看一下slf4j-api.2.0.10包的类结构
如无特别说明,本文中引入的slf4j依赖都是基于slf4j-api.2.0.10版本的
关于slf4j的maven依赖有很多,比如:
Hello world!
引入maven依赖,如果想使用slf4j,必须引用slf4j-api。这里使用的log4j版本号是2.0.10
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.10</version>
</dependency>
DEMO如下:
package com.matio.slf4j.helloworld;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class.getName());
logger.info("Hello slf4j!");
System.out.println("Logger对象:" + logger);
}
}
打印结果如下:
红色部分是slf4j的警告信息,具体可以看看
org.slf4j.helpers.Reporter类
这打印结果可能不会跟我们预期的有出入,那是因为我们目前只引入了slf4j-api这一个依赖包,运行时找不到slf4j的providers,导致了我们生成的最终的logger对象是NOPLogger,而它只是一个空实现。
如果我们想打印日志到console上,可以额外再引入一个包slf4j-simple,它只是slf4j的一个很简单的日志实现,没什么强大的功能,所以使用很少:
<!--slf4j内置简单的实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.10</version>
</dependency>
再次运行Hello world,发现这次日志可以正确输出了,而且生成的logger对象是SimpleLogger
而
SimpleLogger这个类是slf4j-simple包中的类。
slf4j-simple-2.0.10的包结构如下:
据此我们可以猜测:生成的哪种logger实例是由引入的依赖决定的。比如:
slf4j + jul
移除掉slf4j-simple依赖,额外再引入slf4j-jdk14依赖,生成的logger对象类是JDK14LoggerAdapter
slf4j + log4j
移除掉slf4j-simple依赖,额外再引入slf4j-log4j12依赖,生成的logger对象类是Reload4jLoggerAdapter
接下来我们来看看slf4j的底层实现,先一句话总结下:
slf4j 2.x版本 是基于spi机制实现的,
低版本则是基于类加载实现的
slf4j日志绑定原理
Hello world!代码如下:
package com.matio.slf4j.helloworld;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class.getName());
logger.info("Hello slf4j!");
}
}
获取org.slf4j.Logger子实现
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
ILoggerFactory是一个专门生产logger实例的工厂,它里面就只有一个getLogger()方法。所以需要我们先找到ILoggerFactory的具体实现类,这个具体实现类不是在slf4j-api中,而是在jul、log4j、logback等各类日志对接slf4j的实现包当中。那么该如何获取ILoggerFactory实现呢,我们继续看下去:
public static ILoggerFactory getILoggerFactory() {
return getProvider().getLoggerFactory();
}
static SLF4JServiceProvider getProvider() {
// 移除了一些不必要的代码,更简洁些...
INITIALIZATION_STATE = 1;
performInitialization();
switch (INITIALIZATION_STATE) {
case 1:
return SUBST_PROVIDER;
case 3:
return PROVIDER;
case 4:
return NOP_FALLBACK_SERVICE_PROVIDER;
}
}
private static final void performInitialization() {
bind();
if (INITIALIZATION_STATE == 3) {
versionSanityCheck();
}
}
原来ILoggerFactory子实现是通过SLF4JServiceProvider类获取的,直接看bind()方法
private static final void bind() {
try {
// 读取系统属性slf4j.provider
// 利用spi读取所有SLF4JServiceProvider子实现
List<SLF4JServiceProvider> providersList = findServiceProviders();
reportMultipleBindingAmbiguity(providersList);
if (providersList != null && !providersList.isEmpty()) {
// 只获取第一个
PROVIDER = (SLF4JServiceProvider)providersList.get(0);
// 调用其初始化方法
PROVIDER.initialize();
INITIALIZATION_STATE = 3;
reportActualBinding(providersList);
} else {
INITIALIZATION_STATE = 4;
// 读取classpath中所有org/slf4j/impl/StaticLoggerBinder.class类
// 因为在低版本中log4j门面实现主要是依靠该类,比如slf4j-simple.1.7.21、slf4j-jdk14.1.7.25
// 而且他们的包名都是org/slf4j/impl
// 通过StaticLoggerBinder类调用其getLoggerFactory()方法也可以获取到ILoggerFactory子实现
// 因为在新版本中该类已经过期,所以只是输出个告警日志,并没有用StaticLoggerBinder去获取ILoggerFactory子实现
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
}
postBindCleanUp();
} catch (Exception var2) {
failedBinding(var2);
throw new IllegalStateException("Unexpected initialization failure", var2);
}
}
上面代码看着很多,其实绝大多数都在打日志,主要看findServiceProviders()和findPossibleStaticLoggerBinderPathSet()这两个方法就够了。
那么如何获取SLF4JServiceProvider子实现呢?具体看findServiceProviders()方法:
- 读取系统属性
slf4j.provider对应的value(从 2.0.9 开始,可以通过“slf4j.provider”系统属性显式指定提供程序类。这绕过了用于查找提供者的服务加载器机制,并可能缩短 SLF4J 初始化时间。) - 利用SPI机制读取
META-INF\services\org.slf4j.spi.SLF4JServiceProvider文件中所有SLF4JServiceProvider子实现。如果有多个则取第一个
通过反射将classname生成最终的SLF4JServiceProvider子实现(系统属性中优先级最高),然后调用其initialize()方法进行初始化。
如果发现SLF4JServiceProvider子实现一个都没有,那调用findPossibleStaticLoggerBinderPathSet()方法读取classpath中所有org/slf4j/impl/StaticLoggerBinder.class类,然后打印告警日志。其实这个方法可以是slf4j低版本中门面模式的主要实现,在高版本中已经过期了。原因如下:
因为在低版本中log4j门面实现主要是依靠StaticLoggerBinder这个类的,比如slf4j-simple.1.7.21、slf4j-jdk14.1.7.25等,而且这些jar包中StaticLoggerBinder.class类的包名都是org/slf4j/impl;
通过StaticLoggerBinder类调用其getLoggerFactory()方法也可以获取到ILoggerFactory子实现;
因为在新版本中该类已经过期,所以这里只是输出个告警日志,StaticLoggerBinder已经被废弃了。
从 2.0.0 版本开始,SLF4J中的bindings称为provider。尽管如此,总体思路仍然是一样的。SLF4J API 版本 2.0.0 依赖SPI机制来查找其日志记录后端。有关更多详细信息,这是总体思路的图形说明。
上面这副图就表明了 SLF 是如何绑定其它的主流日志框架。
针对上面的编号进行介绍:
①:SLF4J
单独导入slf4j-api是没有日志打印的效果,只会打印几句提示信息,提示未绑定日志实现,因为底层没有绑定具体的日志框架
②:SLF4J+logback
底层绑定logback日志实现框架
③:SLF4J+reload4j(Log4j)
底层绑定Log4j日志实现框架(reload4j是Log4j的升级版,因为之前Log4j出现了重大漏洞)
④:SLF4J+JUL
底层绑定Java自带的JUL日志实现框架
⑤:SLF4J+Simple
底层绑定SLF4J官方推出的基本日志实现框架
⑥:SLF4J+nop
关闭一切日志输出信息
下面按照顺序依次介绍及使用
如果 Java 应用中需要使用日志记录的话,则首先需要引入 slf4j-api.jar 的依赖,统一日志接口。并且,它需要引入具体的实现,共有三种情况:
- 没有引入具体的实现:那么,日志功能将不能起作用
- 引入了一类实现(上图的蓝色框:
slf4j-logback、slf4j-simple、slf4j-nop),由于它们的设计比 SLF 要晚,所以默认遵循 SLF 的规范,只需要导入它们的依赖就可使用 - 引入了另一类实现(
log4j、jdk14),它们的设计比 SLF 要早,在设计之初,并没有遵循 SLF 的规范,无法直接进行绑定。所以,需要添加一个适配层 Adaptation layer。通过适配器进行适配,从而间接地遵循了 SLF 的规范。
使用 SLF 的日志绑定流程:
1、添加 slf4j-api 的依赖
2、使用 slf4j 的 API 在项目中进行统一的日志记录
3、 绑定具体的日志实现
1、绑定了已经实现sfl4j的日志框架,直接添加对应的依赖 2、绑定了没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
4、slf4j 有且仅有一个日志实现框架的绑定(如果出现多个,默认使用第一个依赖日志实现)
slf4j和各种日志实现框架组合
slf4j + slf4j
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.10</version>
</dependency>
<!--slf4j内置简单的实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.10</version>
</dependency>
slf4j-simple日志包支持的功能非常简单,仅支持输出到控制台或某个文件中,适合slf4j初学者,使用场景较少.slf4j-simple-2.0.10.jar源码包结构如下:
执行流程大致如下:
SimpleServiceProvider#initialize();
new SimpleLoggerFactory();
SimpleLogger.lazyInit();
SimpleLoggerConfiguration#init();
最终会调用到SimpleLoggerConfiguration#init();这里来,该方法主要负责加载配置文件,读取用户自定义配置,代码如下:
void init() {
// 加载resources下simplelogger.properties配置文件
this.loadProperties();
String defaultLogLevelString = this.getStringProperty("org.slf4j.simpleLogger.defaultLogLevel", (String)null);
if (defaultLogLevelString != null) {
this.defaultLogLevel = stringToLevel(defaultLogLevelString);
}
this.showLogName = this.getBooleanProperty("org.slf4j.simpleLogger.showLogName", true);
this.showShortLogName = this.getBooleanProperty("org.slf4j.simpleLogger.showShortLogName", false);
this.showDateTime = this.getBooleanProperty("org.slf4j.simpleLogger.showDateTime", false);
this.showThreadName = this.getBooleanProperty("org.slf4j.simpleLogger.showThreadName", true);
this.showThreadId = this.getBooleanProperty("org.slf4j.simpleLogger.showThreadId", false);
dateTimeFormatStr = this.getStringProperty("org.slf4j.simpleLogger.dateTimeFormat", DATE_TIME_FORMAT_STR_DEFAULT);
this.levelInBrackets = this.getBooleanProperty("org.slf4j.simpleLogger.levelInBrackets", false);
this.warnLevelString = this.getStringProperty("org.slf4j.simpleLogger.warnLevelString", "WARN");
this.logFile = this.getStringProperty("org.slf4j.simpleLogger.logFile", this.logFile);
this.cacheOutputStream = this.getBooleanProperty("org.slf4j.simpleLogger.cacheOutputStream", false);
this.outputChoice = computeOutputChoice(this.logFile, this.cacheOutputStream);
if (dateTimeFormatStr != null) {
this.dateFormatter = new SimpleDateFormat(dateTimeFormatStr);
}
}
主要分两步:
- 加载resources目录下
simplelogger.properties配置文件 - 优先从
系统属性中读取上述配置,其次才会从simplelogger.properties文件中读取
resources目录simplelogger.properties文件内容大致如下,具体参考SimpleLoggerConfiguration#init();:
# @see org.slf4j.simple.SimpleLoggerConfiguration.init()
# 入口:SimpleServiceProvider.initialize();实例化SimpleLoggerFactory对象时会读取到该文件完成初始化配置
# 以下属性也支持从系统属性中读取,从系统属性中读取优先级高
# 默认的日志级别,默认info,支持trace、debug、info、warn、error、off
org.slf4j.simpleLogger.defaultLogLevel=info
# 默认true
org.slf4j.simpleLogger.showLogName=true
# 默认false
org.slf4j.simpleLogger.showShortLogName=false
# 是否显示调用线程名称,默认false
org.slf4j.simpleLogger.showThreadName=true
# 默认false
org.slf4j.simpleLogger.showThreadId=false
# 是否显示日期,跟dateTimeFormat搭配使用,默认false
org.slf4j.simpleLogger.showDateTime=true
# 日期格式
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss SSS
# 默认false
org.slf4j.simpleLogger.levelInBrackets=false
org.slf4j.simpleLogger.warnLevelString=WARN
# 默认System.err,支持System.out和自定义保存日志的路径
org.slf4j.simpleLogger.logFile=System.err
org.slf4j.simpleLogger.cacheOutputStream=false
这些属性也支持从系统属性中读取,而且从系统属性中读取的优先级更高
slf4j + jul
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.10</version>
</dependency>
<!--绑定jdk14实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.10</version>
</dependency>
slf4j-jdk14底层日志是jul,不需要额外引入依赖。slf4j-jdk14-2.0.10.jar源码包结构如下:
执行流程大致如下:
org.slf4j.jul.JULServiceProvider#initialize();
java.util.logging.Logger.getLogger("");
后续就是jul中逻辑了,可以参考juejin.cn/post/734586…
这里需要特别说明的是:因为jul出现要比slf4j要早,所以并没有遵循slf4j的规范,无法直接进行绑定。所以,需要添加一个适配层 Adaptation layer。通过适配器进行适配,从而间接地遵循了slf4j的规范。比如:
所以我们通过
org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(App.class);获取到的logger对象其实是JDK14LoggerAdapter,它可以看成是java.util.logging.Logger的装饰器
更多关于java util logging可以参考我之前写的文章:
slf4j + log4j
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.10</version>
</dependency>
<!--绑定log4j实现,需要导入适配器-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.10</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
因为log4j是最早出现的日志框架,slf4j出现的时间肯定要比log4j晚,所以slf4j为了适配log4j,需要增加额外的适配层,也就是slf4j-log4j12。而且log4j不同于jul,它是第三方框架,所以还需要引入log4j包。
这里我们也要注意下slf4j-log4j12的版本问题,比如这里引入的版本是2.0.10。此版本引入的slf4j-reload4j-2.0.10.jar,而更低版本的可能就不会,具体可以参考slf4j-log4j12的pom.xml。
slf4j-log4j12-2.0.10.jar源码包结构如下:
更多关于log4j可以参考我之前写的文章:
slf4j + logback
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.10</version>
</dependency>
<!-- logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.6</version>
</dependency>
关于logback底层的文章还在撰写中,后续会更新。
slf4j + log4j2
注意版本问题
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<!--使用log4j2的适配器进行绑定-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.10.0</version>
</dependency>
<!--log4j2的日志门面-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<!--log4j2的日志实现-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.10.0</version>
</dependency>
slf4j 禁用日志
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.10</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.10</version>
</dependency>
这个禁用原理就更简单了,就是生成一个logger,里面啥都没干。
slf4j项目实战
案例一
一个项目,一个模块用log4j,另一个模块用slf4j+log4j2,如何统一输出?
其实在某些中小型公司,这种情况很常见。我曾经见过某公司的项目,因为研发不懂底层的日志原理,日志文件里头既有log4j.properties,又有log4j2.xml,各种API混用,惨不忍睹!
还有人用着jul的API,然后拿着log4j.properties,跑来问我,为什么配置不生效!简直是一言难尽!
OK,回到我们的问题,如何统一输出!OK,这里就要用上slf4j的适配器,slf4j提供了各种各样的适配器,用来将某种日志框架委托给slf4j。其最明显的集成工作方式有如下:
进行选择填空,将我们的案例里的条件填入,根据题意应该选log4j-over-slf4j适配器,于是就变成下面这张图
就可以实现日志统一为log4j2来输出!
ps:根据适配器工作原理的不同,被适配的日志框架并不是一定要删除!以上图为例,log4j这个日志框架删不删都可以,你只要能保证log4j的加载顺序在log4j-over-slf4j后即可。因为log4j-over-slf4j这个适配器的工作原理是,内部提供了和log4j一模一样的api接口,因此你在程序中调用log4j的api的时候,你必须想办法让其走适配器的api。如果你删了log4j这个框架,那你程序里肯定是走log4j-over-slf4j这个组件里的api。如果不删log4j,只要保证其在classpth里的顺序比log4j前即可!
案例二
如何让spring以log4j2的形式输出?
spring默认使用的是jcl输出日志,由于你此时并没有引入Log4j的日志框架,jcl会以jul做为日志框架。此时集成图如下
而你的应用中,采用了slf4j+log4j-core,即log4j2进行日志记录,那么此时集成图如下
那我们现在需要让spring以log4j2的形式输出?怎么办?
OK,第一种方案,走jcl-over-slf4j适配器,此时集成图就变成下面这样了
在这种方案下,spring框架中遇到日志输出的语句,就会如上图红线流程一样,最终以log4J2的形式输出!
OK,有第二种方案么?
有,走jul-to-slf4j适配器,此时集成图如下
ps:这种情况下,记得在代码中执行
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
这样jul-to-slf4j适配器才能正常工作,详情可以查询该适配器工作原理。
天啦噜!要死循环
假设,我们在应用中调用了sl4j-api,但是呢,你引了四个jar包,slf4j-api-xx.jar,slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar,于是你就会出现如下尴尬的场面
如上图所示,在这种情况下,你调用了slf4j-api,就会陷入死循环中!slf4j-api去调了slf4j-log4j12,slf4j-log4j12又去调用了log4j,log4j去调用了log4j-over-slf4j。最终,log4j-over-slf4j又调了slf4j-api,陷入死循环!
以上部分内容参考: mp.weixin.qq.com/s/8VvBdRH_Y…