文章

Java log

Java log相当混乱……其实和Java io的设计有点儿像,混乱起源于“层层转包”。其实搞清楚“实现类”和“包装类”,整个问题差不多就迎刃而解了。

  1. 实现类
    1. jul
    2. log4j
    3. log4j2
    4. logback
  2. 包装类
    1. slf4j
      1. slf4j + log4j
      2. slf4j + log4j2
      3. slf4j + logback
      4. slf4j同时拥有多个实现者
    2. jcl
      1. jcl + jul
      2. jcl + log4j
      3. jcl + log4j2
      4. jcl + logback
  3. 层层转包
    1. slf4j + jcl + log4j
    2. 老系统,新log
  4. 总结
    1. slf4j解决性能问题

实现类

java log的实现类有:

  • jul:java.util.logging,jdk自带的log实现;
  • log4j:第三方实现;
  • log4j2:log4j升级版;
  • logback:新型log实现框架;

直接使用这些log实现者是很简单的。

jul

不需要任何依赖,不需要任何配置,有jdk就够了。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.logging.Logger;

/**
 * @author liuhaibo on 2021/08/10
 */
public class JavaUtilLoggingDemo {

    private static final Logger logger= Logger.getLogger(JavaUtilLoggingDemo.class.getName());

    public static void main(String[] args){
        logger.info("jdk logging info: a msg");
    }
}

Logger是JDK里的java.util.logging.Logger,是一个具体的类

jul是支持配置的,默认是jre目录下的lib/logging.properties文件,也可以自定义修改系统属性java.util.logging.config.file,具体参考java.util.logging.LogManager的实现。

log4j

需要引入log4j依赖:

1
2
3
4
5
6
7
    <dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>

log4j的功能就丰富很多,比如定义不同的appender、按照time和size做rolling等。功能丰富,自然需要进行一些个性化定制。

log4j可以配置log4j.xml:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd" >
<log4j:configuration debug="false">

    <!--Console appender -->
    <appender name="stdout" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
        </layout>
    </appender>

    <!-- File appender -->
    <appender name="fileout" class="org.apache.log4j.FileAppender">
        <param name="file" value="target/log4j/fileout.log"/>
        <param name="append" value="true"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %m%n"/>
        </layout>
    </appender>

    <appender name="byName2333" class="org.apache.log4j.FileAppender">
        <param name="file" value="target/log4j/invokeByName.log"/>
        <param name="append" value="true"/>
        <layout class="org.apache.log4j.PatternLayout">
            <!-- 时间;%5p输出trace/debug/info等,右对齐,添加'-'就是左对齐;%c{1}貌似是日志名;行号;内容;换行-->
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %5p %c{1}:%L -  %m%n"/>
        </layout>
    </appender>

    <!-- Rolling appenders -->
    <appender name="roll-by-size" class="org.apache.log4j.RollingFileAppender">
        <param name="file" value="target/log4j/roll-by-size.log"/>
        <param name="MaxFileSize" value="5KB"/>
        <param name="MaxBackupIndex" value="2"/> <!-- It's one by default. -->
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %m%n"/>
        </layout>
    </appender>
    <appender name="roll-by-size-2" class="org.apache.log4j.RollingFileAppender">
        <param name="file" value="target/log4j/roll-by-size-2.log"/>
        <param name="MaxFileSize" value="5KB"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %m%n"/>
        </layout>
    </appender>

    <!--Override log level for specified package -->
    <!-- 通过设定包名,直接设置日志 -->
    <category name="example.logging.log4j">
        <priority value="TRACE"/>
    </category>

    <!-- 通过设定类名,直接设置日志 -->
    <category name="Log4jRollingDemo">
        <priority value="TRACE"/>
        <appender-ref ref="roll-by-size"/>
        <appender-ref ref="roll-by-size-2"/>
    </category>

    <!-- 代码中,通过调用此日志名称来调用日志 -->
    <logger name="invokeByName">
		<level value="INFO" />
		<appender-ref ref="byName2333" />
	</logger>

    <root>
        <level value="INFO"/>
        <appender-ref ref="stdout"/>
        <appender-ref ref="fileout"/>
    </root>

</log4j:configuration>

也可以配置log4j.properties

1
2
3
4
log4j.rootLogger = trace, console
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} %m%n

xml比properties优先级更高一些。具体可以参考实现类org.apache.log4j.LogManager

使用log4j:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.log4j.Logger;

/**
 * @author liuhaibo on 2017/12/02
 */
public class Log4jDemo {

    private static final Logger log = Logger.getLogger(Log4jDemo.class);
    private static final Logger learnFromGorgon = Logger.getLogger("invokeByName");

    public static void main(String[] args) {
        log.trace("Trace log message");
        log.debug("Debug log message");
        log.info("Info log message");
        log.error("Error log message");

        learnFromGorgon.info("This is learnt from Gorgon~");
    }
}

使用rolling log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.apache.log4j.Logger;

/**
 * @author liuhaibo on 2017/12/02
 */
public class Log4jRollingDemo {

    private final static Logger log = Logger.getLogger(Log4jRollingDemo.class);

    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 2000; i++){
            log.info("This is the " + i + " time I say 'Hello World'.");
            Thread.sleep(10);
        }
    }
}

Logger是org.apache.log4j.Logger,是一个具体的类

log4j2

log4j2如果只是log4j的下一个版本,那就没啥好说的了。但是它飘了,它不想只当一个log实现者,它还想当一个log的统一者。所以它的Logger不是一个具体类,而是一个接口。如果别的日志实现者使用了它的Logger接口,就可以纳入其框架下

所以它的依赖是分成两块的:一个纯api,供它的实现者使用。一个是它的api的自有实现:

1
2
3
4
5
6
7
8
9
10
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.10.0</version>
        </dependency>

log4j2并不叫log4j2,而是groupId比log4j1的groupId多了个logging。

这里我们先拿它的自有实现举例,把它当一个log的实现者来看待。

和log4j类似,它的配置方式log4j2.xml

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        # Console appender
        <Console name="stdout" target="SYSTEM_OUT">
            # Pattern of log message for console appender
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
        </Console>

        # File appender
        <File name="fileout" fileName="target/log4j2/fileout.log"
              immediateFlush="false" append="true">
            # Pattern of log message for file appender
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
        </File>

        # Rolling appender
        <RollingFile name="roll-by-size"
                     fileName="target/log4j2/roll-by-size.log" filePattern="target/log4j2/roll-by-size.%i.log"
                     ignoreExceptions="false">
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <OnStartupTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="5 KB"/>
            </Policies>
        </RollingFile>

        <RollingFile name="roll-by-time"
                     fileName="target/log4j2/roll-by-time.log"
                     filePattern="target/log4j2/roll-by-time.%d{yyyy-MM-dd-HH-mm}.log"
                     ignoreExceptions="false">
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</Pattern>
            </PatternLayout>
            <TimeBasedTriggeringPolicy/>
        </RollingFile>

        <RollingFile name="roll-by-time-and-size"
                     fileName="target/log4j2/roll-by-time-and-size.log"
                     filePattern="target/log4j2/roll-by-time-and-size.%d{yyyy-MM-dd-HH-mm}.%i.log"
                     ignoreExceptions="false">
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <OnStartupTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="5 KB"/>
                <TimeBasedTriggeringPolicy/>
            </Policies>
            <DefaultRolloverStrategy>
                <Delete basePath="${baseDir}" maxDepth="2">
                    <IfFileName glob="target/log4j2/roll-by-time-and-size.*.log"/>
                    <IfLastModified age="20s"/>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
    </Appenders>

    <Loggers>
        # Override log level for specified package
        <Logger name="example.logging.log4j2" level="TRACE"/>

        <Logger name="Log4j2RollingDemo"
                level="TRACE">
            <AppenderRef ref="roll-by-size"/>
            <AppenderRef ref="roll-by-time"/>
            <AppenderRef ref="roll-by-time-and-size"/>
        </Logger>

        <Root level="DEBUG">
            <AppenderRef ref="stdout"/>
            <AppenderRef ref="fileout"/>
        </Root>
    </Loggers>
</Configuration>

不同的是它没有properties配置方式。

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * @author liuhaibo on 2017/12/02
 */
public class Log4j2Demo {

    private static final Logger log = LogManager.getLogger(Log4j2Demo.class);

    public static void main(String[] args) {
        log.trace("Trace log message");
        log.debug("Debug log message");
        log.info("Info log message");
        log.error("Error log message");
    }
}

Logger是org.apache.logging.log4j.Logger,是一个接口,而不是一个具体的类

另外需要注意的是log4j的2和1的package name变了,2比1的包名多了一个logging。这样挺好的,既然不兼容,即使同时引入log4j的1和2也会因为包名不同,不会发生包冲突。

作为log的具体实现者,log4j2到这里就要结束了。

但是它还是一个log接口,可以有别的实现者。那就有一个新的问题:如果有多个该log接口的实现者同时被引入了,采用谁作为具体实现?

可以查看log4j2的实现:先找log4j2.component.properties,再找META-INF/log4j-provider.properties,如果都没找到,就用默认的SimpleLoggerContextFactory。

其实我认为怎么实现的不重要,知道它作为接口,有哪些活要干就行了。 接下来介绍其他log接口框架,以及框架间的相互转换,只要对这个问题清楚了,就不会晕了。具体转换的小细节不重要。

logback

logback虽然是一个纯粹的log实现者,但是它依附于slf4j这个log接口框架。所以介绍logback离不开slf4j接口。

需要引入slf4j接口:

1
2
3
4
5
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>

还要引入接口的实现者,logback:

1
2
3
4
5
6
7
8
9
10
11
        <!-- slf4j + logback binding -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>

实际上只引入一个logback-classic就行了,通过它间接引入了logback-core和logback-classic。

logback也需要配置,logback.xml

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<configuration>
    # Console appender
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            # Pattern of log message for console appender
            <Pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</Pattern>
        </layout>
    </appender>

    # File appender
    <appender name="fileout" class="ch.qos.logback.core.FileAppender">
        # Name of a log file
        <file>target/slf4j/fileout.log</file>
        <append>true</append>
        <encoder>
            # Pattern of log message for file appender
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</pattern>
        </encoder>
    </appender>

    # Rolling appenders
    <appender name="roll-by-size"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>target/slf4j/roll-by-size.log</file>
        <rollingPolicy
                class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>target/slf4j/roll-by-size.%i.log.zip
            </fileNamePattern>
            <minIndex>1</minIndex>
            <maxIndex>3</maxIndex>
            <totalSizeCap>1MB</totalSizeCap>
        </rollingPolicy>
        <triggeringPolicy
                class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>5KB</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <appender name="roll-by-time"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>target/slf4j/roll-by-time.log</file>
        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>target/slf4j/roll-by-time.%d{yyyy-MM-dd-HH-mm}.log.zip
            </fileNamePattern>
            <maxHistory>20</maxHistory>
            <totalSizeCap>1MB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</pattern>
        </encoder>
    </appender>

    <appender name="roll-by-time-and-size"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>target/slf4j/roll-by-time-and-size.log</file>
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>target/slf4j/roll-by-time-and-size.%d{yyyy-MM-dd-mm}.%i.log.zip
            </fileNamePattern>
            <maxFileSize>5KB</maxFileSize>
            <maxHistory>20</maxHistory>
            <totalSizeCap>1MB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</pattern>
        </encoder>
    </appender>

    <!-- From: click-consumer -->
    <appender name="FILE_SEND_SUCCEED" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>target/slf4j/send-succeed.log.%d{yyyy-MM-dd}</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
        </encoder>
    </appender>

    # Override log level for specified package
    <logger name="LogbackRollingDemo" level="TRACE">
        <appender-ref ref="roll-by-size"/>
        <appender-ref ref="roll-by-time"/>
        <appender-ref ref="roll-by-time-and-size"/>
    </logger>

    <!-- get by name -->
    <!-- IMPORTANT: additivity -->
    <logger name="send.succeed" additivity="false">
        <appender-ref ref="FILE_SEND_SUCCEED" />
    </logger>

    <logger name="console" additivity="false">
        <appender-ref ref="stdout"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="stdout"/>
        <appender-ref ref="fileout"/>
    </root>
</configuration>

使用的代码实际上只有slf4j:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author liuhaibo on 2017/12/02
 */
public class LogbackDemo {

    private static final Logger log = LoggerFactory.getLogger(LogbackDemo.class);

    public static void main(String[] args) {
        log.debug("Debug log message");
        log.info("Info log message");
        log.error("Error log message");
    }
}

Logger是slf4j的org.slf4j.Logger,是一个接口。

和log4j2作为log接口一样,slf4j是怎么找到具体实现类的?在后面介绍。

包装类

有些人不干具体的活,只是把工作委托给他人,可以认为是包装类。

log接口框架就不干具体的活儿,只负责找到接口的具体实现者,再交给他去做就行了。

log接口框架主要有两个:

  • slf4j:比较流行的log接口框架;
  • jcl:java apache commons logging,apache制定的log接口框架。

上面说到slf4j2也能做log接口框架,也算是一个吧。不过不常用。

使用包装类,心里要有一个概念:表面上我们是在使用它,实际上我们使用的是底层的实现者

slf4j

上面已经提到了slf4j,logback是它的一个默认实现

它还可以有其他实现,比如默认简单实现:

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

那么它怎么找到具体的实现类的呢?

slf4j的实现比较粗暴:只认org.slf4j.impl.StaticLoggerBinder,就用它返回的LogFactory,获取Logger。

所以:

  • logback在logback-classic包里有一个org.slf4j.impl.StaticLoggerBinder类,最终返回的slf4j Logger接口是使用logback实现的。
  • slf4j-simple这个简单默认实现在slf4j-simple包里有一个org.slf4j.impl.StaticLoggerBinder类,最终返回的slf4j Logger接口是SimpleLogger实现的。

slf4j + log4j

log4j也是具体的log实现者,slf4j能不能基于它实现log?能,但是需要满足两个条件:

  1. 编码的时候,使用slf4j的接口,仿佛我们在使用slf4j;
  2. slf4j的实现类,要使用log4j。也就是说要有一个StaticLoggerBinder,返回的是基于log4j的log factory;

但是log4j里并没有org.slf4j.impl.StaticLoggerBinder类,怎么办?

那就做个用于桥接的东西,把log4j包装一下,让包装类满足上述两个条件 就行了。这个用于包装log4j的包叫做slf4j-log4j12

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

        <!-- slf4j + log4j12 binding -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>

log4j12指的是它包装的是log4j 1.2.x,而不是log4j1和log4j2……

这个包里有一个org.slf4j.impl.StaticLoggerBinder类,最终返回的slf4j的ILoggerFactory接口实现是Log4jLoggerFactory,它返回的slf4j Logger接口实现是Log4jLoggerAdapter,它内部包装着log4j的Logger,所有的活儿都委托给log4j的Logger去做。

既然log4j是具体实现,只需要使用log4j.xml配置log4j就可以了。

至于代码,因为是基于接口slf4j写的,所以和logback基于slf4j的代码一模一样,根本不用变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author liuhaibo on 2017/12/02
 */
public class Slf4jLog4j12Demo {

    private static final Logger log = LoggerFactory.getLogger(Slf4jLog4j12Demo.class);

    public static void main(String[] args) {
        log.debug("Debug log message");
        log.info("Info log message");
        log.error("Error log message");
    }
}

slf4j + log4j2

同理,log4j2也是一个具体实现类,所以slf4j也可以基于它实现log。

同样的问题,log4j2也没有实现slf4j的接口,所以也有一个包专门做这件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.14.1</version>
        </dependency>
    </dependencies>

而它做的事情简直就是slf4j-log4j12的复刻。

配置一个log4j2.xml就行了,代码也是完全不用变。

slf4j + logback

上面已经介绍过logback了。它是slf4j接口的最常用实现。

值得一提的是,它的依赖其实也是分成了两部分:

1
2
3
4
5
6
7
8
9
10
11
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
  • logback-core是log具体实现类,相当于上面的log4j和log4j2。
  • logback-classic相当于起桥接作用的slf4j-log4j12log4j-slf4j-impl

不过logback-core本来就是依附于slf4j的接口实现的,所以logback-classic并不需要再搞个slf4j的包装类Logger实现。

配置一个logback.xml就行了,代码也是完全不用变。

slf4j同时拥有多个实现者

slf4j没有实现者不行,无法打印log。实现者多了也头疼,到底该用哪一个呢?

slf4j实际上会挑一个,并在log里输出警告信息。以下就是既有logback-classic,又有slf4j-simple的情况:

1
2
3
4
5
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/xxx/.m2/repository/org/slf4j/slf4j-simple/1.7.25/slf4j-simple-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/xxx/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.SimpleLoggerFactory]

碰到这种情况,把不需要的实现者依赖删掉就行了。

jcl

理解了slf4j,其实就理解jcl了。

它也是一个log接口,也需要具体的实现,也需要找到具体的实现者。

只有在如何找到具体的实现者这个问题上,commons log和slf4j有着不同的方式。

1
2
3
4
5
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>

jcl包里有以下四种org.apache.commons.logging.Log接口的实现类:

1
2
3
4
5
6
private static final String[] classesToDiscover = {
    "org.apache.commons.logging.impl.Log4JLogger",
    "org.apache.commons.logging.impl.Jdk14Logger",
    "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
    "org.apache.commons.logging.impl.SimpleLog"
};

显然,他们和上面说过的Log4jLoggerAdapter一模一样,就是一个单纯的包装类,实现了jcl的Log接口,所有工作都委托给内部包装的具体log,自己并不需要做什么。

Jdk14Logger指的是基于Jdk 1.4的一个log实现。现在jdk 14已经出了,这个名字尴尬了……

至于jcl怎么找到具体是哪个LogFactory实现类(LogFactory确定了,Log实现也就确定了),比slf4j的粗暴实现稍微正常一点儿:

  1. 从系统属性中获取:System.getProperty("org.apache.commons.logging.LogFactory")
  2. 使用java的SPI机制从META-INF/services/org.apache.commons.logging.LogFactory搜索;
  3. commons-logging.properties配置文件中搜索;
  4. 使用默认实现。

这些细节都不重要。

jcl + jul

基于jdk的log实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author liuhaibo on 2021/08/10
 */
public class CommonsLoggingDemo {

    private static final Log log = LogFactory.getLog(CommonsLoggingDemo.class);

    public static void main(String[] args) {
        log.trace("Trace log message");
        log.debug("Debug log message");
        log.info("Info log message");
        log.error("Error log message");
    }
}

事实上,log4j是最先有的java日志框架,apache出的。后来java1.4仿照log4j写了自己的jul,于是apache出了jcl,面向接口编程,如果有log4j就用log4j,没有就用jul,避免了混乱。

这就是为什么jcl默认会使用jul。

jcl + log4j

依赖:

1
2
3
4
5
6
7
8
9
10
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

等等,不需要像slf4j一样桥接log4j?需要是需要,只不过桥接的代码已经在commons-logging包里了 (jcl包里已经有了四种log的包装类),所以不需要再引入单独的桥接代码了。

commons logging:你不接入我?我接入你!事实上,jcl的出现就是为了统一slf4j和jul的,所以内部有他们的默认桥接实现。

配置log4j.xml,代码全都是jcl的类,不用变动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author liuhaibo on 2021/08/10
 */
public class CommonsLoggingLog4j1Demo {

    private static final Log log = LogFactory.getLog(CommonsLoggingLog4j1Demo.class);

    public static void main(String[] args) {
        log.trace("Trace log message");
        log.debug("Debug log message");
        log.info("Info log message");
        log.error("Error log message");
    }
}

jcl + log4j2

引入jcl和log4j2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
        
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.2</version>
        </dependency>

jcl并没有桥接log4j2,所以还需要引入桥接的包:

1
2
3
4
5
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-jcl</artifactId>
            <version>2.2</version>
        </dependency>

具体作用和之前一样:一个包装类包装一下slf4j2的logger鹅已。

配置log4j2.xml,代码全都是jcl的类,不用变动。

jcl + logback

引入jcl和logback。logback强依赖于slf4j接口,所以其实就是引入slf4j + logback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
        
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.12</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.3</version>
        </dependency>

把slf4j桥接到jcl上:

1
2
3
4
5
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.12</version>
        </dependency>

套娃的感觉来了!logback接到slf4j上,slf4j再桥接到jcl上。

配置logback.xml,代码全都是jcl的类,不用变动。

这种组合最大的特色就是:slf4j本身就是接口,现在这种接口又要桥接到另一种接口commons log上了。

那么问题来了:同样都是接口,jcl能桥接到slf4j上吗?

当然可以。

层层转包

桥接可以无限转接下去,就像一个usb口转接为了typec口,又转接为了一个网口,从而成功接入网线,实现了上网的功能……

slf4j + jcl + log4j

假设现在我们想最上层想使用slf4j的接口编写代码,底层想使用jcl,jcl实际上又是用了log4j,也就是说log4j是最终的log实现者,要怎么做?

首先,底层使用log4j,实现log输出功能:

1
2
3
4
5
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

然后把jcl接到log4j上,因为jcl已经有了桥接log4j的代码,所以只引入jcl就行了:

1
2
3
4
5
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

slf4j再桥接到jcl上:

1
2
3
4
5
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jcl</artifactId>
            <version>1.7.32</version>
        </dependency>

这个包叫slf4j-jcljcl-over-slf4j正好反过来。

话说这名字起的也真够不对称的……

当然还可以有其他桥接方式,比如log4j2也是接口,它可以作为顶层,把刚刚的slf4j + jcl + log4j作为自己的底层。这样的话就需要一个log4j2 over slf4j的包,实际上还真有这么个东西:

1
2
3
4
5
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.32</version>
</dependency>

更多桥接:http://www.slf4j.org/legacy.html

老系统,新log

但实际上我们一般都不按照上面的例子这么玩——为什么要使用一个新log接口搭配上老旧的log实现呢?一般来讲,新log实现性能会更好一些。所以反着玩是有可能的——一些旧系统使用了老的log,我们把他们“架空”,底层实际上使用新log作为真正的实现类:

正如slf4j举的例子: legacy

如图左上角,系统中引入的第三方依赖很可能使用了jcl的接口编程,也可能使用了log4j和jdk log。假设我们的系统使用slf4j+logback,那么我们只会配置logback,比如把日志打到logs/log这个文件里,而这些依赖所使用的那些形形色色的log实现并不会把日志打到logs/log

有没有办法让他们也打到logs/log里?

第一种思路,考虑给jcl、log4j、jul全都配置一遍,让他们也把log打到logs/log里。这么做不可取,一者麻烦,二者如果有些log不支持配置文件,怎么配……

第二种思路,就是上面说的把那些log全都桥接到logback上(实际就是桥接到slf4j上),这样一来,表面上用的还是那些log,实际上用的都是logback。而我们已经配置了logback打log到logs/log,所以所有的日志最终都输出到了logs/log

最终我们需要桥接三次:

  • 使用jcl-over-slf4j让jcl实际使用slf4j;
  • 使用log4j-over-slf4j让log4j实际使用slf4j;
  • 使用jul-to-slf4j让jul实际使用slf4j;

引入这些桥接依赖为什么就能将其基本实现替换为slf4j呢?不能。至少,你得把原来的实现删了吧。所以对于slf4j和jcl,需要把原来的依赖删掉,替换成log4j-over-slf4j或者jcl-over-slf4j。本质上,这两个包基于slf4j(也就是封装了slf4j)分别实现了一种slf4j、jcl的具体实现类。但是jul呢?jul是嵌在jdk里的,总不能把jdk删了吧……所以jul的需要额外配置

1
2
3
4
5
6
    // Optionally remove existing handlers attached to j.u.l root logger
    SLF4JBridgeHandler.removeHandlersForRootLogger();  // (since SLF4J 1.6.5)

    // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during
    // the initialization phase of your application
    SLF4JBridgeHandler.install();

或者在jul的配置文件里,配置为使用基于slf4j的jul实现:

1
2
3
    // Installation via logging.properties configuration file:
    // register SLF4JBridgeHandler as handler for the j.u.l. root logger
    handlers = org.slf4j.bridge.SLF4JBridgeHandler

这些在slf4j legacy上都有详细描述。在github repo里有详细示例,最终无论使用那种log,都基于slf4j,并把log都打到了logs/log文件夹里。

Ref:

  • 总览:https://my.oschina.net/pingpangkuangmo?tab=newest&catalogId=3292361
    • https://my.oschina.net/pingpangkuangmo/blog/406618
    • https://my.oschina.net/pingpangkuangmo/blog/407895
    • https://my.oschina.net/pingpangkuangmo/blog/408382
    • https://my.oschina.net/pingpangkuangmo/blog/410224

总结

最后梳理一下各框架出现的时间线:

  • https://zhuanlan.zhihu.com/p/86249472
  1. log4j by apache;
  2. jul by jdk,抄的 log4j;
  3. jcl by apache,接口框架,旨在统一jul和log4j;
  4. log4j的作者离开了apache,写了slf4j,旨在解决jcl的性能问题。作者同时写了logback作为其默认实现,性能优于其他框架。还有一堆桥接包,让其他log桥接到slf4j上;
  5. apache重写log4j为log4j2,具有logback的所有特性。

slf4j解决性能问题

任意级别的log,都有两种不同的log方式。比如info:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    /**
     * Log a message at the INFO level.
     *
     * @param msg the message string to be logged
     */
    public void info(String msg);
    
    /**
     * Log a message at the INFO level according to the specified format
     * and argument.
     * <p/>
     * <p>This form avoids superfluous object creation when the logger
     * is disabled for the INFO level. </p>
     *
     * @param format the format string
     * @param arg    the argument
     */
    public void info(String format, Object arg);

第二个方法,即使不先判断isInfoEnabled也不会创建多余的对象。

应该是内部进行了isInfoEnabled判断,如果没开启,就免了把object和string拼起来了。比如slf4j使用log4j作为底层实现时,桥接类Log4jLoggerAdapter是这么实现这个avoid superfluous object接口的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /**
     * Log a message at level DEBUG according to the specified format and
     * argument.
     * 
     * <p>
     * This form avoids superfluous object creation when the logger is disabled
     * for level DEBUG.
     * </p>
     * 
     * @param format
     *          the format string
     * @param arg
     *          the argument
     */
    public void debug(String format, Object arg) {
        if (logger.isDebugEnabled()) {
            FormattingTuple ft = MessageFormatter.format(format, arg);
            logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
        }
    }

其实就是拼接object之前先判断一下isDebugEnabled。

有点儿类似于Map的putIfAbsent和computeIfAbsent的区别。前者无论是否put,object已经创建好了。后者传的是个函数,需要put的话再compute出来一个object。

同时我们也明白了一个道理:面向接口编程,能统一各种不同的实现。如果别的实现没有接入你的这套接口,那就搞一个包装类,包装该实现,并implement你自己的接口,就成了一个桥接器,从而将该实现纳入自己的接口框架之下。

本文由作者按照 CC BY 4.0 进行授权