Java log
Java log相当混乱……其实和Java io的设计有点儿像,混乱起源于“层层转包”。其实搞清楚“实现类”和“包装类”,整个问题差不多就迎刃而解了。
实现类
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?能,但是需要满足两个条件:
- 编码的时候,使用slf4j的接口,仿佛我们在使用slf4j;
- 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-log4j12
和log4j-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的粗暴实现稍微正常一点儿:
- 从系统属性中获取:
System.getProperty("org.apache.commons.logging.LogFactory")
; - 使用java的SPI机制从
META-INF/services/org.apache.commons.logging.LogFactory
搜索; - 从
commons-logging.properties
配置文件中搜索; - 使用默认实现。
这些细节都不重要。
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-jcl
和jcl-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作为真正的实现类:
如图左上角,系统中引入的第三方依赖很可能使用了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
- log4j by apache;
- jul by jdk,抄的 log4j;
- jcl by apache,接口框架,旨在统一jul和log4j;
- log4j的作者离开了apache,写了slf4j,旨在解决jcl的性能问题。作者同时写了logback作为其默认实现,性能优于其他框架。还有一堆桥接包,让其他log桥接到slf4j上;
- 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你自己的接口,就成了一个桥接器,从而将该实现纳入自己的接口框架之下。