介绍

官网:https://www.slf4j.org/

SLF4J全称Simple Logging Facade for Java,简单日志门面,这个不是具体的日志解决方案,而是通过门面模式提供一些Java Logging API,类似于JCL。作者当时创建SLF4J的目的就是为了替代Jakarta Commons Logging(JCL)。Slf4j本身只提供了一个slf4j-api-version.jar包,这个jar中主要是日志的抽象接口,jar中本身并没有对抽象出来的接口做实现。

  1. 主要意义是提供interface,具体的实现可以交由其他日志框架。
  2. 当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。(但这个自带的简单的实现不在slf4j的jar包中,需要额外引入)
  3. 对于一般的Java项目而言,日志框架会选择slf4j-apis作为门面,配上具体的日志实现框架,中间使用桥接器完成桥接。所以我们可以得出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
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>

编写第一个程序

1
2
3
4
5
6
7
8
9
10
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {

public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}

运行该程序,会输出如下:

1
2
3
4
5
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.

## 要解决此问题,您需要添加任一日志记录框架绑定

引入slf4j的简单实现 slf4j-simple-2.0.7.jar

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>

## 此时未进行日志的任何配置 重新运行后可以看到打印的日志信息
[main] INFO pers.fulsun.log.HelloWorld - Hello World

参数化{}输出日志

有时应用中,充满了大量的各种日志级别的输出语句,当我们调高日志级别时,虽然此时低级别的日志不会被输出,但其实语句内的内容也被进行了计算。如:

1
2
3
4
5
6
7
8
9
10
// 语句内的内容也被进行了计算,就比较耗性能。
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

// 尽管我们可以这样
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

// 但我们更推荐:虽然性能更快,但代价是非标准和不太灵活的。
logger.debug("The entry is {}.", entry);

绑定不同的日志实现

SLF4J适配与桥接

为什么需要桥接?

因为在slf4j之前,已经早就出现log4jJUL两个日志框架,这两个框架肯定没有实现slf4j的接口的。(但logback和log4j2是比slf4j迟的,他们是实现了slf4j的接口),所以我们需要一种叫“桥接”的技术。将别的门面桥接到slf4j中。

logbackslf4j-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-apislf4j-log4j放到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
    33
    private 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-slf4jslf4j-log4jjul-to-slf4jslf4j-jdk14,这个很好理解,会有死循环,启动也会报错。图中的红线就表示互斥关系

  • 当然slf4j也提供了可以把对slf4j的调用桥接到JCL上的工程包—slf4j-jcl,可以看出slf4j的设计者考虑非常周到,想想这样的情况:遗留系统使用的是JCL+log4j,因为系统功能演进,依赖了其他业务线的库,恰好那个库依赖了slf4j-api,并且应用需要关心这个库的日志,那么就需要转接日志到JCL上即可。细心的你可能一经发现,slf4j-jcljcl-over-slf4j也是互斥的。

  • 对于log4j2的加入,也很简单,和logback是很相似的,红线依然表示依赖的互斥,当然log4j-slf4j-impl也会和其它日志实现如logback-classicslf4j-log4jslf4j-jdk14互斥。

适配说明

适配机制允许开发人员在使用SLF4J接口记录日志时,实际上将日志事件路由到所选择的底层日志实现。SLF4J为不同的底层日志实现提供了适配器模块,这些适配器模块作为桥梁将SLF4J的日志接口与特定的底层日志库连接起来。每个适配器模块对应于一个特定的底层日志库,例如slf4j-logbackslf4j-log4jslf4j-julslf4j-log4j12等。适配器(slf4j-log4j12)允许通过SLF4J接口使用Log4j作为日志实现。

适配使用案例

SLF4J+SLF4J自带的简单实现

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>

SLF4J+logback

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.11</version>
</dependency>

只留下logback,那么slf4j门面使用的就是logback日志实现。值得一提的是,这一次没有多余的提示信息,所以在实际应用的时候,我们一般情况下,仅仅只是做一种日志实现的集成就可以了。

SLF4J+nop

使用slf4j-nop,表示不记录日志。这个实现依赖与logback和simple是属于一类的,通过集成依赖的顺序而定,所以如果想要让nop发挥效果,禁止所有日志的打印,那么就必须要将slf4j-nop的依赖放在所有日志实现依赖的上方。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.7</version>
</dependency>

SLF4J+log4j

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- 导入log4j适配器依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

加入配置 log4j.properties配置文件。

1
2
3
4
5
6
7
8
# Root logger option
log4j.rootLogger=INFO, stdout

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

SLF4J+JUL

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.7</version>
</dependency>

SLF4J+logback+slf4j-simple

在真实生产环境中,slf4j只要绑定一个日志实现框架就可以了,如果绑定多个日志实现,默认使用导入依赖的第一个,而且会产生没有必要的警告信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.11</version>
</dependency>

所以此时虽然集成了logback,可以看到提示你包含了多个SLF4J的绑定,并且此时实际的绑定是SimpleLogger。

可以调整导入的位置,先导入logback-classic,此时实际绑定的是logback

桥接使用案例

log4j重构为slf4j + logback

假设我们项目一直以来使用的是log4j日志框架,但是随着技术和需求的更新换代,log4j己然不能够满足我们系统的需求,我们现在就需要将系统中的日志实现重构为slf4j+logback的组合。在不触碰java源代码的情况下,将这个问题给解决掉。

1
2
3
4
5
6
7
8
9
import org.apache.log4j.Logger;

public class HelloWorld {
static Logger logger = Logger.getLogger(HelloWorld.class);

public static void main(String[] args) {
logger.info("Hello World");
}
}
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
<!-- 移除log4j的依赖 -->
<!--<dependency>-->
<!-- <groupId>log4j</groupId>-->
<!-- <artifactId>log4j</artifactId>-->
<!-- <version>1.2.17</version>-->
<!--</dependency>-->

<!-- 将slf4j日志门面和logback的日志实现依赖加入进来 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.11</version>
</dependency>

<!-- 此时就需要添加桥接器的依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>2.0.7</version>
</dependency>

注意:日志没有用之前我们配置的log4j.properties的配置文件,而是用logback默认的配置。

桥接注意事项

在使用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
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.7</version>
</dependency

测试在2.0.7能正常打印日志

1
2
3
4
5
6
SLF4J: Class path contains multiple SLF4J providers.
SLF4J: Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@19469ea2]
SLF4J: Found provider [org.slf4j.reload4j.Reload4jServiceProvider@13221655]
SLF4J: See https://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual provider is of type [ch.qos.logback.classic.spi.LogbackServiceProvider@19469ea2]
03:55:02.924 [main] INFO pers.fulsun.HelloWorld -- Hello World

在1.7版本(1.7.25)下回提示异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SLF4J: Detected both log4j-over-slf4j.jar AND bound slf4j-log4j12.jar on the class path, preempting StackOverflowError.
SLF4J: See also http://www.slf4j.org/codes.html#log4jDelegationLoop for more details.
java.lang.ExceptionInInitializerError
at org.slf4j.impl.StaticLoggerBinder.<init>(StaticLoggerBinder.java:72)
at org.slf4j.impl.StaticLoggerBinder.<clinit>(StaticLoggerBinder.java:45)
at org.slf4j.LoggerFactory.bind(LoggerFactory.java:150)
at org.slf4j.LoggerFactory.performInitialization(LoggerFactory.java:124)
at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:412)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:357)
at org.apache.log4j.Category.<init>(Category.java:57)
at org.apache.log4j.Logger.<init>(Logger.java:37)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
at org.apache.log4j.Logger.getLogger(Logger.java:41)
at org.apache.log4j.Logger.getLogger(Logger.java:49)
at pers.fulsun.HelloWorld.<clinit>(HelloWorld.java:6)
Caused by: java.lang.IllegalStateException: Detected both log4j-over-slf4j.jar AND bound slf4j-log4j12.jar on the class path, preempting StackOverflowError. See also http://www.slf4j.org/codes.html#log4jDelegationLoop for more details.
at org.slf4j.impl.Log4jLoggerFactory.<clinit>(Log4jLoggerFactory.java:54)
... 12 more
Exception in thread "main"

桥接器在下方,适配器在上方,此时不会报错,会正常打印。(使用适配器)

因为此时桥接器会失效,即没有执行,也没有执行logback,此时导入桥接器对于原本要重构日志的目的来说没有任何意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.11</version>
</dependency>
<!-- 加入对log4j的适配会 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<!-- 此时就需要添加桥接器的依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>