Java日志-SLF4J使用与源码分析
SLF4J全称The Simple Logging Facade for Java,Java简易日志门面,将接口抽象与实现隔离开,在不修改代码的情况下使用不同的日志实现。
SLF4J支持的日志实现有:
- log4j
- logback(推荐实现)
- java.util.logging
- simple(全部输出到System.err)
- Jakarta Commons Logging
- nop(忽略所有日志)
使用SLF4J
只要在项目中引入SLF4J的jar包就能开启SLF4J:
1 | <dependency> |
然后写一个最简单的输出日志的程序:
1 | import org.slf4j.Logger; |
程序运行会在控制台打印:
1 | SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". |
因为slf4j在classpath中没有找到任何一个slf4j binding,所以会提示一个错误信息,并提示会使用NOP logger,这个logger的行为就是忽略所有输出。
绑定日志实现
之前提到了slf4j支持很多日志实现,slf4j包含了一些日志实现的桥接库,称为SLF4J bindings
,官方提供的binding有:
slf4j-log4j12-1.8.0-beta2.jar
log4j1.2.x的binding,应该是使用最广的了。需要引入log4j。
Binding for log4j version 1.2, a widely used logging framework. You also need to place log4j.jar on your class path.slf4j-jdk14-1.8.0-beta2.jar
JDK1.4提供的java.util.logging
的bindingslf4j-nop-1.8.0-beta2.jar
NOP的binding,忽略所有日志slf4j-simple-1.8.0-beta2.jar
简单日志实现,输出所有日志到System.err,只会输出大于等于INFO级别的日志。小程序可以用这个实现。slf4j-jcl-1.8.0-beta2.jar
Jakarta Commons Logging日志库的binding,这个binding会代理所有的日志操作到JCL。JCL也是一个日志门面,但是目前已经被slf4j取代了。logback-classic-1.0.13.jar
(requires logback-core-1.0.13.jar)
这是slf4j的官方日志实现(其实log4j,slf4j,logback都是一家出品),logback就是按照slf4j的API直接实现的,所以不需要中间的binding。所以用这个官方实现,中间的损耗也是最小的。
切换日志实现,只要使用不同的binding jar包即可。不同于JCL,slf4j没有使用类加载器,而是在binding中硬绑定具体的实现。所以classpath中同时只能存在一个实现的binding。所以slf4j没有JCL可能的类加载器问题和内存损耗问题。
slf4j1.6之前,如果没有找到binding,slf4j会抛出NoClassDefFoundError
异常,1.6之后,即使没有binding,slf4j也不会抛出异常,只是提示没有找到binding。所以对于库或者框架的作者来说,一定不要在项目中添加具体的slf4j binding,只要添加slf4j本身即可,让用户有机会选择具体的实现。
slf4j,slf4j binding,日志实现之间的关系见下图:
所以官方推荐的使用方法是只要在pom引入具体的slf4j binding依赖即可,slf4j binding会引入slf4j-api和具体的日志实现的依赖,而且版本都不会有问题。手动引入这些当然也是可以,只是要注意这三者的版本兼容性。
我们引入slf4j-log4j12
依赖:
1 | <dependency> |
再次运行结果:
1 | log4j:WARN No appenders could be found for logger (com.mushan.blog.Main). |
可以看到提示的是log4j没有配置的信息,slf4j已经成功使用log4j实现了。
关于参数
上面的例子,使用到了{}
占位符,这是slf4j的参数占位符,效果与使用+
进行字符串拼接是一样的,项目中也经常会看到使用+
的例子,这是不好的,因为有性能问题。
比如这样的一个日志语句:
1 | logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i])); |
为了调用debug函数,需要执行这个字符串拼接语句,需要把i
和entry[i]
转为字符串,然后拼接。如果日志没有开启,或者日志级别高于debug,日志是不需要打印的,但是这个拼接消耗却是没有办法避免的。
所以日志框架后面想出了一个办法:
1 | if(logger.isDebugEnabled()) { |
先使用logger.isDebugEnabled()
方法检测对应的日志级别是否打开,打开了我才调用日志打印函数。但是这样做的问题是:1.麻烦,每次需要打印日志都要写一句检测判断 2.如果日志是开启了debug日志级别,但是日志本身是disable的话,依然有不必要的参数拼接消耗。
所有slf4j推荐的使用方式是:
1 | Object entry = new SomeObject(); |
日志实现在必要的时候才会替换日志中的{}
占位符为具体的参数,所有不会有无意义的消耗。
按slf4j官网的说法,下面两种写法在日志被禁用的情况下,性能查了30倍(有点意外):
1 | logger.debug("The new entry is "+entry+"."); |
如果你需要在日志中输出{}
本身,可以使用\
进行转义:
1 | logger.debug("Set \\{} differs from {}", "3"); |
这样会输出:Set {} differs from 3
。
SLF4J源码分析
slf4j是如何实现部署时绑定日志实现呢?我们来分析一下他的代码,以下分析基于slf4j 1.7.25。
顺便说一句,在网上看到很多分析slf4j的文章,得到的结论是使用类加载器来加载具体实现,这个是完全错误的。slf4j的官网已经明确说明slf4j不使用任何类加载器,这是他的一个优点,不会有类加载器冲突,不会有内存占用问题。
先看一下slf4j的整体类图:
我们获取Logger的方法是LoggerFactory.getLogger(name)
,所以入口方法就是这个工厂方法:
1 | public static Logger getLogger(String name) { |
可以看出LoggerFactory不是真正的日志类工厂,真正的日志类工厂获取流程如下:
1 | // 使用一个变量表示当前的初始化状态,因为可能多线程同时初始化,所以该状态变量声明为volatile |
我们进一步来看slf4j如何绑定日志实现:
1 | private final static void performInitialization() { |
最关键的一句话是最简单的一句话:
1 | StaticLoggerBinder.getSingleton(); |
这里slf4j要求所有的binding必须实现一个org.slf4j.impl.StaticLoggerBinder
类,slf4j就用最普通的方式实例化这个类。存在三种情况:
- classpath中不存在这个类。这种情况是没有添加任何binding的情况,这种情况下这句话抛出
NoClassDefFoundError
异常,slf4j捕获异常,返回NOPLoggerFactory。 - classpath存在这个类,且只有一个。slf4j实例化这个类,并调用
StaticLoggerBinder.getSingleton().getLoggerFactory()
方法得到具体的实现的LoggerFactory。 - classpath存在多个同样全限定名的类。JVM是允许这种情况的,这种情况下,会使用更靠前的那个类,因为JVM是从前往后搜索类的。slf4j在这种情况下,为了提醒用户,会答应出classpath存在的类,与最终使用的binding。
所以,通过这种方式,slf4j不需要自定义类加载器就能绑定不同的日志实现。优点是实现简单,性能高,兼容性高,缺点是无法在运行时切换日志实现,不过这个基本上也用不到。
还有一个问题,slf4j-api项目本身,存在org.slf4j.impl.StaticLoggerBinder
这个类吗?不存在的话,编译是没法通过的,如果存在这个类,可能会覆盖binding中的类,这个问题如何解决?
在slf4j-api项目的pom中发现了这么一个配置:
1 | <plugin> |
使用maven的ant插件,在打包前删除了target/classes/org/slf4j/impl
下的class文件,这样发布出去的slf4j就不存在这个类了,真是太机智了。
总结
- slf4j是Java简易日志门面,可以在不修改代码的情况下,在部署时使用不同的日志实现
- slf4j支持参数化消息,在日志关闭的情况下,减少不必要的字符串拼接和类型转换,提高性能
- slf4j使用静态绑定的方式绑定具体的日志binding,依赖binding实现的
org.slf4j.impl.StaticLoggerBinder
类,没有使用自定义类加载器 - slf4j允许classpath存在多个slf4j binding,但是只会使用其中一个