Slf4j的使用
介绍
SLF4J全称Simple Logging Facade for Java,简单日志门面,这个不是具体的日志解决方案,而是通过门面模式提供一些Java Logging API,类似于JCL。作者当时创建SLF4J的目的就是为了替代Jakarta Commons Logging(JCL)。Slf4j本身只提供了一个slf4j-api-version.jar包,这个jar中主要是日志的抽象接口,jar中本身并没有对抽象出来的接口做实现。
- 主要意义是提供interface,具体的实现可以交由其他日志框架。
- 当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。(但这个自带的简单的实现不在slf4j的jar包中,需要额外引入)
- 对于一般的Java项目而言,日志框架会选择
slf4j-api
s作为门面,配上具体的日志实现框架,中间使用桥接器完成桥接。所以我们可以得出SLF4J最重要的两个功能就是对于日志框架的绑定
以及日志框架的桥接
。
快速使用
启用SLF4J意味着你的库/应用只增加了一个强制依赖,即
slf4j-api-2.0.7.jar
。slf4j-api在1.8 以上版本 jar 包时,需要和 slf4j-nop.jar slf4j-simple.jar, slf4j-log4j12.jar, slf4j-jdk14.jar or logback-classic.jar 中的任意一个结合使用
引入依赖
1 | <dependency> |
编写第一个程序
1 | import org.slf4j.Logger; |
运行该程序,会输出如下:
1 | SLF4J: No SLF4J providers were found. |
引入slf4j的简单实现 slf4j-simple-2.0.7.jar
1 | <dependency> |
参数化{}输出日志
有时应用中,充满了大量的各种日志级别的输出语句,当我们调高日志级别时,虽然此时低级别的日志不会被输出,但其实语句内的内容也被进行了计算。如:
1 | // 语句内的内容也被进行了计算,就比较耗性能。 |
绑定不同的日志实现
SLF4J适配与桥接
为什么需要桥接?
因为在slf4j之前,已经早就出现log4j
和JUL
两个日志框架,这两个框架肯定没有实现slf4j的接口的。(但logback和log4j2是比slf4j迟的,他们是实现了slf4j的接口),所以我们需要一种叫“桥接”的技术。将别的门面桥接到slf4j中。
logback
是slf4j-api
的天然实现,不需要桥接包就可以使用,同时实现了变参占位符日志输出方式等等新特性。另外slf4j还封装了很多其他的桥接包,可以使用到其他的日志实现中,比如slf4j-log4j12
,就可以使用log4j进行底层日志输出,再比如slf4j-jdk14
,可以使用JUL进行日志输出。
桥接(Bridge): 桥接是指将SLF4J日志消息传递到不同的底层日志实现(如Logback、Log4j)的机制。这是通过在SLF4J和底层日志库之间添加中间层的逻辑来实现的,从而将SLF4J的日志事件转发到适当的底层实现。这使得你可以在不更改应用程序代码的情况下,切换底层的日志实现。桥接的目的是实现日志的平滑迁移和交互。
适配器(Adapter): 适配器是指为SLF4J提供不同底层日志实现的绑定或接口。SLF4J提供了不同的适配器模块,用于将SLF4J的日志事件绑定到特定的底层实现。适配器的作用是连接SLF4J的通用接口和特定的底层日志库,使得应用程序可以通过SLF4J进行日志记录,而底层的实现可以根据适配器的配置来决定使用哪个日志库。
桥接说明
在实际环境中我们经常会遇到不同的组件使用的日志框架不同的情况,例如Spring Framework使用的是日志组件是Commons Logging,XSocket依赖的则是Java Util Logging。当我们在同一项目中使用不同的组件时应该如果解决不同组件依赖的日志组件不一致的情况呢?现在我们需要统一日志方案,统一使用Slf4j,把他们的日志输出重定向到Slf4j,然后Slf4j又会根据绑定器把日志交给具体的日志实现工具。Slf4j带有几个桥接模块,可以重定向Log4j,JCL和java.util.logging中的Api到Slf4j。
log4j-over-slf4j-version.jar | 将Log4j重定向到Slf4j |
---|---|
jcl-over-slf4j-version.jar | 将Commons Logging里的Simple Logger重定向到slf4j |
jul-to-slf4j-version.jar | 将Java Util Logging重定向到Slf4j |
为了使使用JCL等其他日志系统后者实现的用户可以很简单地切换到slf4j上来,给出了各种桥接工程。下边用一个图来表示下这个家族的大致成员
如上图,最上层表示桥接层,下层表示具体的实现层,中间是接口层。
- 可以看出这个图中所有的jar都是围绕着slf4j-api活动的,其中slf4j-jul的jar名称是slf4j-jdk14。
slf4j-api和具体的实现层是怎么绑定的呢?
这个其实是在编译时绑定的,这个可能不好理解,最直接的表达方式是不需要像jcl那样配置一下,只需要把
slf4j-api
和slf4j-log4
j放到classpath上,即实现绑定。原理可以下载slf4j-api的源码查看,这个设计还是很巧妙的.slf4j-api
中会去调用StaticLoggerBinder这个类获取绑定的工厂类,而每个日志实现会在自己的jar中提供这样一个类,这样slf4j-api就实现了编译时绑定实现。但是这样接口的源码编译需要依赖具体的实现了,不太合理吧?当时我也有这样的迷惑,因为打开slf4j-api的jar,看不到StaticLoggerBinder,就查看了slf4j-api的源码,在源码中看到了StaticLoggerBinder这个类,猜想应该是slf4j-api在打包过程中有动作,删除了自己保重的那个类,结果不出所料,确实是pom中的ant-task给处理了,pom中处理方式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<tasks>
<echo>Removing slf4j-api's dummy StaticLoggerBinder and StaticMarkerBinder</echo>
<delete dir="target/classes/org/slf4j/impl"/>
</tasks>
</configuration>
</plugin>slf4j-log4j
,logback-classic
,slf4j-jdk14
这三个日志实现是不能同时和slf4j共存的,也就是说只能有一个实现存在,不然启动会提示有多个绑定,判断多个实现的代码也很简单,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
private static void singleImplementationSanityCheck() {
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class
.getClassLoader();
Enumeration paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader
.getResources(STATIC_LOGGER_BINDER_PATH);
}
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order during iteration
Set implementationSet = new LinkedHashSet();
while (paths.hasMoreElements()) {
URL path = (URL) paths.nextElement();
implementationSet.add(path);
}
if (implementationSet.size() > 1) {
Util.report("Class path contains multiple SLF4J bindings.");
Iterator iterator = implementationSet.iterator();
while(iterator.hasNext()) {
URL path = (URL) iterator.next();
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
}同时上面这个图中桥接层和对应的实现jar是不能共存的,比如
log4j-over-slf4j
和slf4j-log4j
,jul-to-slf4j
和slf4j-jdk14
,这个很好理解,会有死循环,启动也会报错。图中的红线就表示互斥关系。当然slf4j也提供了可以把对slf4j的调用桥接到JCL上的工程包—
slf4j-jcl
,可以看出slf4j的设计者考虑非常周到,想想这样的情况:遗留系统使用的是JCL+log4j,因为系统功能演进,依赖了其他业务线的库,恰好那个库依赖了slf4j-api,并且应用需要关心这个库的日志,那么就需要转接日志到JCL上即可。细心的你可能一经发现,slf4j-jcl
和jcl-over-slf4j
也是互斥的。对于log4j2的加入,也很简单,和logback是很相似的,红线依然表示依赖的互斥,当然
log4j-slf4j-impl
也会和其它日志实现如logback-classic
、slf4j-log4j
、slf4j-jdk14
互斥。
适配说明
适配机制允许开发人员在使用SLF4J接口记录日志时,实际上将日志事件路由到所选择的底层日志实现。SLF4J为不同的底层日志实现提供了适配器模块,这些适配器模块作为桥梁将SLF4J的日志接口与特定的底层日志库连接起来。每个适配器模块对应于一个特定的底层日志库,例如slf4j-logback
、slf4j-log4j
、slf4j-jul
、slf4j-log4j12
等。适配器(slf4j-log4j12)允许通过SLF4J接口使用Log4j作为日志实现。
适配使用案例
SLF4J+SLF4J自带的简单实现
1 | <dependency> |
SLF4J+logback
1 | <dependency> |
只留下logback,那么slf4j门面使用的就是logback日志实现。值得一提的是,这一次没有多余的提示信息,所以在实际应用的时候,我们一般情况下,仅仅只是做一种日志实现的集成就可以了。
SLF4J+nop
使用slf4j-nop,表示不记录日志。这个实现依赖与logback和simple是属于一类的,通过集成依赖的顺序而定,所以如果想要让nop发挥效果,禁止所有日志的打印,那么就必须要将slf4j-nop的依赖放在所有日志实现依赖的上方。
1 | <dependency> |
SLF4J+log4j
1 | <dependency> |
加入配置 log4j.properties
配置文件。
1 | # Root logger option |
SLF4J+JUL
1 | <dependency> |
SLF4J+logback+slf4j-simple
在真实生产环境中,slf4j只要绑定一个日志实现框架就可以了,如果绑定多个日志实现,默认使用导入依赖的第一个,而且会产生没有必要的警告信息。
1 | <dependency> |
所以此时虽然集成了logback,可以看到提示你包含了多个SLF4J的绑定,并且此时实际的绑定是SimpleLogger。
可以调整导入的位置,先导入logback-classic
,此时实际绑定的是logback
桥接使用案例
log4j -> slf4j+log4j2
一个项目,一个模块用log4j,另一个模块用slf4j+log4j2,如何统一输出?
这里就要用上slf4j的适配器,slf4j提供了各种各样的适配器,用来将某种日志框架委托给slf4j。其最明显的集成工作方式有如下:
进行选择填空,将我们的案例里的条件填入,根据题意应该选log4j-over-slf4j适配器,于是就变成下面这张图
就可以实现日志统一为log4j2来输出!
根据适配器工作原理的不同,被适配的日志框架并不是一定要删除!以上图为例,log4j这个日志框架删不删都可以,你只要能保证log4j的加载顺序在log4j-over-slf4j后即可。因为log4j-over-slf4j这个适配器的工作原理是,内部提供了和log4j一模一样的api接口,因此你在程序中调用log4j的api的时候,你必须想办法让其走适配器的api。如果你删了log4j这个框架,那你程序里肯定是走log4j-over-slf4j这个组件里的api。如果不删log4j,只要保证其在classpth里的顺序比log4j前即可!
log4j重构为slf4j + logback
假设我们项目一直以来使用的是log4j日志框架,但是随着技术和需求的更新换代,log4j己然不能够满足我们系统的需求,我们现在就需要将系统中的日志实现重构为slf4j+logback的组合。在不触碰java源代码的情况下,将这个问题给解决掉。
1 | import org.apache.log4j.Logger; |
1 | <!-- 移除log4j的依赖 --> |
注意:日志没有用之前我们配置的log4j.properties的配置文件,而是用logback默认的配置。
Spring4以log4j2的形式输出
Spring4默认使用的是jcl输出日志,由于你此时并没有引入Log4j的日志框架,jcl会以jul做为日志框架。此时集成图如下
而你的应用中,采用了slf4j+log4j-core,即log4j2进行日志记录,那么此时集成图如下
第一种方案,走jcl-over-slf4j适配器,此时集成图就变成下面这样了
在这种方案下,spring框架中遇到日志输出的语句,就会如上图红线流程一样,最终以log4J2的形式输出!
第二种方案: 走jul-to-slf4j适配器,此时集成图如下
这种情况下,记得在代码中执行,
1 | SLF4JBridgeHandler.removeHandlersForRootLogger(); |
这样jul-to-slf4j适配器才能正常工作,详情可以查询该适配器工作原理。
桥接注意事项
在使用Slf4j桥接时要注意避免形成死循环,在项目依赖的jar包中不要存在以下情况。
多个日志jar包形成死循环的条件 | 产生原因 |
---|---|
log4j-over-slf4j.jar和slf4j-log4j12.jar同时存在 | 由于slf4j-log4j12.jar的存在会将所有日志调用委托给log4j。但由于同时由于log4j-over-slf4j.jar的存在,会将所有对log4j api的调用委托给相应等值的slf4j,所以log4j-over-slf4j.jar和slf4j-log4j12.jar同时存在会形成死循环 |
jul-to-slf4j.jar和slf4j-jdk14.jar同时存在 | 由于slf4j-jdk14.jar的存在会将所有日志调用委托给jdk的log。但由于同时jul-to-slf4j.jar的存在,会将所有对jul api的调用委托给相应等值的slf4j,所以jul-to-slf4j.jar和slf4j-jdk14.jar同时存在会形成死循环 |
互斥演示
在有slf4j-api, log4j-over-slf4j, logback的依赖的基础上,加入适配log4j的依赖:
1 | <dependency> |
测试在2.0.7能正常打印日志
1 | SLF4J: Class path contains multiple SLF4J providers. |
在1.7版本(1.7.25)下回提示异常
1 | SLF4J: Detected both log4j-over-slf4j.jar AND bound slf4j-log4j12.jar on the class path, preempting StackOverflowError. |
桥接器在下方,适配器在上方,此时不会报错,会正常打印。(使用适配器)
因为此时桥接器会失效,即没有执行,也没有执行logback,此时导入桥接器对于原本要重构日志的目的来说没有任何意义。
1 | <dependency> |
日志框架选择
如果是在一个新的项目中建议使用Slf4j与Logback组合,这样有如下的几个优点。
Slf4j实现机制决定Slf4j限制较少,使用范围更广。由于Slf4j在编译期间,静态绑定本地的LOG库使得通用性要比Commons Logging要好。
Logback拥有更好的性能。Logback声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高。这个操作在Logback中需要3纳秒,而在Log4J中则需要30纳秒。LogBack创建记录器(logger)的速度也更快:13毫秒,而在Log4J中需要23毫秒。更重要的是,它获取已存在的记录器只需94纳秒,而Log4J需要2234纳秒,时间减少到了1/23。跟JUL相比的性能提高也是显著的。
Commons Logging开销更高
1
2
3
4
5
6
7
8
9
10// 在使Commons Logging时为了减少构建日志信息的开销,通常的做法是
if(log.isDebugEnabled()){
log.debug("User name: " +
user.getName() + " buy goods id :" + good.getId());
}
// 在Slf4j阵营,你只需这么做:
log.debug("User name:{} ,buy goods id :{}", user.getName(),good.getId());
// 也就是说,Slf4j把构建日志的开销放在了它确认需要显示这条日志之后,减少内存和Cup的开销,使用占位符号,代码也更为简洁Logback文档免费。Logback的所有文档是全面免费提供的,不象Log4J那样只提供部分免费文档而需要用户去购买付费文档。