Java中的日志框架

很久之前我简单整理过相关笔记,刚开始认为日志框架只是很简单的存在,然而事实上并不简单,尤其在不同的依赖中使用不同的框架下,就是一团糟;再加上 SB 的黑盒,如果用的很杂其实还是蛮糟心的,各种冲突时不时跳出来恶心你一下,借此机会重新整理一下。

框架介绍

我们主要讨论那些比较知名的日志框架,例如 commons-logging、log4j、slf4j、logback。
在探讨之前,可以先了解下为什么需要日志框架,最早的框架是怎么演变的,这里推荐一篇文章:一个著名的日志系统是怎么设计出来的?

在 log4j 被 Apache Foundation 收入门下之后,由于理念不合,log4j 的作者 Ceki 离开并开发了 slf4j 和 logback。

下面就一个个的来看。

CommonsLogging/JCL

apache-commons-logging (之前称为 JakartaCommonsLogging 即 JCL)是 Apache 提供的一个通用的日志接口,可以理解为是一个规范。
在 commons-logging 中,有一个 SimpleLogger 的简单实现,但是它功能很弱,所以使用 commons-logging ,通常都是配合着 log4j 来使用;

commons-logging 会通过动态查找的机制,在程序运行时自动找出真正使用的日志库,并且尽可能找到一个”最合适”的日志实现类,如果判断有 Log4j 包,则使用 log4j,最悲观的情况下也总能保证提供一个日志实现 (SimpleLog)

Log4j和Log4j2

它是 Apache 的一个开放源代码项目,实现了输出到控制台、文件、 回滚文件、发送日志邮件、输出到数据库日志表、自定义标签等全套功能,且配置比较简单。

后来 Apache Logging 一直在关门憋大招,log4j2 在 beta 版鼓捣了几年,终于在 2014 年发布了 GA 版,不仅吸收了 logback 的先进功能,更通过优秀的锁机制、LMAX Disruptor、”无垃圾”机制等先进特性,在性能上全面超越了 log4j 和 logback。

log4j2 弃用了 properties 方式配置,采用的是 xml、json 或者 jsn 这种方式来做。

可以说 log4j2 是与 logback 来对标的。

slf4j

slf4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J 是一个用于日志系统的简单 Facade,允许最终用户在部署其应用时使用其所希望的日志系统。

可以这么说,slf4j 等于 commons-logging,是各种日志实现的通用入口,会根据 classpath 中存在下面哪一个 Jar 来决定具体的日志实现库;它只是一个 API,提供一个规范。
因为 slf4j 用的很广泛,所以重点说说,提供一个架构图。

SLF4J (Simple logging Facade for Java) 不是一个真正的日志实现,而是一个抽象层( abstraction layer),它允许你在后台使用任意一个日志类库。
如果是在编写供内外部都可以使用的 API 或者通用类库,那么你真不会希望使用你类库的客户端必须使用你选择的日志类库。

如果一个项目已经使用了 log4j,而你加载了一个类库,比方说 Apache ActiveMQ 它依赖于于另外一个日志类库 logback,那么你就需要把它也加载进去
但如果 Apache Active MQ 使用了 SLF4J,你可以继续使用你的日志类库 (当前是 log4j) 而无需忍受加载和维护一个新的日志框架的痛苦。

slf4j 为各类日志输出服务提供了适配库,如 slf4j-log4j12(log4j 适配器),slf4j-simple(slf4j 简单实现),slf4j-jdk14(适配 JDK 的 Logger)等。
一个 Java 工程下只能引入一个 slf4j 适配库,slf4j 会加载 org.slf4j.impl.StaticLoggerBinder 作为输出日志的实现类。这个类在每个适配库中都存在,当需要更换日志输出服务时(比如从 logback 切换回 log4j),只需要替换掉适配库即可。
我们简单理解为,你使用 slf4j-api 来进行开发,其他人可以通过选用不同的适配器 + 对应的具体日志类的方式来进行各种组合。

slf4j 还推出了 jcl-over-slf4j 桥接库,能够把使用 JCL 的 API 输出的日志桥接到 slf4j 上,方便那些想要使用 slf4j 作为日志门面但同时又要使用 Spring 等需要依赖 JCL 的类库的系统。

logback

logback 是由 log4j 创始人设计的又一个开源日志组件。
logback 当前分成三个模块:logback-core、logback- classic 和 logback-access。

  • logback-core
    是其它两个模块的基础模块。
  • logback-classic
    它是 log4j 的一个 改良版本。此外 logback-classic 完整实现 SLF4J API 使你可以很方便地更换成其它日志系统如 log4j。
  • logback-access
    主要作为一个与 Servlet 容器交互的模块,比如说 tomcat 或者 jetty,提供一些与 HTTP 访问相关的功能。

logback 天然与 slf4j 适配,不需要额外引入适配库(毕竟是一个作者写的)
想在 Java 程序中使用 Logback,需要依赖三个 jar 包,分别是 slf4j-api,logback-core,logback-classic。
其中 slf4j-api 并不是 Logback 的一部分,是另外一个项目,但是强烈建议将 slf4j 与 Logback 结合使用。

补充

在 Java 领域日志工具中,最早得到广泛使用的是 log4j。那么为啥有 commons-logging 的出现?上面已经介绍了 common-logging 只提供 log 的接口,其中具体的实现时动态绑定的,所以 common-logging 与 log4j 的结合比较多!

但是随之也产生了一些问题,那就是 common-logging 的动态绑定有时候也会失败,在这样的背景下 slf4j 应运而生,slf4j 与 commons-logging 一样提供 log 接口,但是 slf4j 是通过静态绑定实现。

slf4j 唯独没有提供 log4j2 的适配库和桥接库,log4j-slf4j-impl 和 log4j-to-slf4j 都是 Apache Logging 自己开发的,看样子 Ceki 和 Apache Logging 的梁子真的很深啊……倒是 Apache 没有端架子,可能也是因为 slf4j 太火了吧

log4j2 和 logback 各有长处,总体来说,如果对性能要求比较高的话,log4j2 相对还是较优的选择

SB中的应用

在 Spring 中使用的是 JCL 框架,SB 中根据 spring-boot-starter-logging 的依赖分析,可以得出:

  • SpringBoot2.x 底层也是使用 slf4j+logback 或 Log4J 的方式进行日志记录。
  • SpringBoot 引入中间替换包把其他的日志都替换成了 SLF4J。
  • 如果我们要引入其他框架、可以把这个框架的默认日志依赖移除掉。

至于 1.x 版本就不再讨论了,也可能是 JCL。
根据不同的日志系统,你可以按如下规则组织配置文件名,就能被正确加载:

  • Logback:logback-spring.xml, logback-spring.groovy, logback.xml, logback.groovy
  • Log4j:log4j-spring.properties, log4j-spring.xml, log4j.properties, log4j.xml
  • Log4j2:log4j2-spring.xml, log4j2.xml
  • JDK (Java Util Logging):logging.properties

门面框架虽然有 slf4j 和 jcl 两类,就目前肯定 slf4j 更受欢迎,那么之前用 jcl 的怎么办,例如 spring,这多亏了 jcl-over-slf4j 桥接器可以进行转换,这都不是事。

使用log4j

也就是目前可以说最常见的组合 slf4j + log4j,先排除后添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>

可以根据日志情况来确认。

使用log4j2

方法其实有很多种,那种好目前我也没评估,举例一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

思路就是排除 SB 自带的日志框架,然后加入我们自己的(通过 starter 方式)

经典使用

就目前来说,用的最广泛的还是 log4j ,毕竟即使后面的框架更好,项目已经进行好几年的情况下贸然换用可能会导致很多奇怪的问题,这也造成来现在的依赖日志框架都是乱七八糟,但总体以 log4j 为主,缺点可以忍受,没有大到需要换框架的地步。

log4j使用

因为 log4j 用的很多,所以重点再说说。
默认会读取资源目录下的 log4j.properties 文件,当然也可以自定义配置文件的位置。
配置文件的基本格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置根 Logger
log4j.rootLogger = [level], appenderName1, appenderName2, …

# 配置日志信息输出目的地 Appender
log4j.appender.appenderName = fully.qualified.name.of.appender.class
log4j.appender.appenderName.option1 = value1

log4j.appender.appenderName.optionN = valueN

# 配置日志信息的格式(布局)
log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class
log4j.appender.appenderName.layout.option1 = value1

log4j.appender.appenderName.layout.optionN = valueN

输出级别

关于 level 日志输出级别,共有五级:

标识ID描述
FATAL0适用于严重错误事件
ERROR3适用于代码存在错误事件
WARN4适用于代码会有潜在错误事件
INFO6适用于代码运行期间
DEBUG7适用于代码调试期间

除此之外还有两种状态就是 ALL: 打开所有日志;OFF:关闭所有日志;

输出目的地

Appender 为日志输出目的地,Log4j 提供的 appender 有以下几种:

1
2
3
4
5
org.apache.log4j.ConsoleAppender(控制台),
org.apache.log4j.FileAppender(文件),
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件),
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件),
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

日志输出格式

Layout:日志输出格式,Log4j提供的layout有以下几种:

1
2
3
4
org.apache.log4j.HTMLLayout(以HTML表格形式布局),
org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

打印参数 Log4J 采用类似 C 语言中的 printf 函数的打印格式格式化日志信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
%m  输出代码中指定的消息
%p 输出优先级,即 DEBUG,INFO,WARN,ERROR,FATAL
%r 输出自应用启动到输出该log信息耗费的毫秒数
%c 输出所属的类目,通常就是所在类的全名
%t 输出产生该日志事件的线程名
%n 输出一个回车换行符,Windows 平台为 "\r\n",Unix 平台为 "\n"
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,
比如:%d{yyyy-MM-dd HH:mm:ss,SSS},输出类似:2017-12-21 13:37:05 512
%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。
举例:Testlog4.main(TestLog4.java: 10 )
[%10p] 右对齐,最小宽度10
[%-10p] 左对齐,最小宽度10

SSS 其实是毫秒的意思

配置参考

具体项目具体对待,仅供参考:

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
### set log levels ###  
log4j.rootLogger = debug, stdout, D, E

### 输出到控制台 ###
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{ABSOLUTE} %5p %c{ 1 }:%L - %m%n

### 输出到日志文件 ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/log.log
log4j.appender.D.Append = true
## 输出DEBUG级别以上的日志
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

### 保存异常信息到单独文件 ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File = logs/error.log
log4j.appender.E.Append = true
## 只输出ERROR级别以上的日志!!!
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %l:%c:%t:%r ] - [ %p ] %m%

log4j2

使用 log4j2 需要两个依赖:

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

配置文件示例,就是格式变成了 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
<?xml version="1.0" encoding="UTF-8"?>    
<configuration status="error">
<!-- 先定义所有的appender -->
<appenders>
<!-- 这个输出控制台的配置 -->
<Console name="Console" target="SYSTEM_OUT">
<!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
<!-- 这个都知道是输出日志的格式 -->
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</Console>

<!-- append 为TRUE表示消息增加到指定文件中,false 表示消息覆盖指定的文件内容,默认值是true -->
<!-- 打印出所有的信息 -->
<File name="log" fileName="log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>

<!-- 添加过滤器ThresholdFilter,可以有选择的输出某个级别以上的类别 onMatch="ACCEPT" onMismatch="DENY"意思是匹配就接受,否则直接拒绝 -->
<File name="ERROR" fileName="logs/error.log">
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>

<!-- 这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
<RollingFile name="RollingFile" fileName="logs/web.log"
filePattern="logs/$${date:yyyy-MM}/web-%d{MM-dd-yyyy}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
<SizeBasedTriggeringPolicy size="2MB"/>
</RollingFile>
</appenders>

<!-- 然后定义logger,只有定义了logger并引入的appender,appender才会生效 -->
<loggers>
<!-- 建立一个默认的root的logger -->
<root level="trace">
<appender-ref ref="RollingFile"/>
<appender-ref ref="Console"/>
<appender-ref ref="ERROR" />
<appender-ref ref="log"/>
</root>
</loggers>
</configuration>

配置其实都差不多,应该还是蛮好看懂的。

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoding>UTF-8</encoding>
<encoder>
<pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>

<appender name="mmall" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--<File>d:/mmalllog/mmall.log</File>-->
<File>/developer/apache-tomcat-7.0.73/logs/mmall.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/developer/apache-tomcat-7.0.73/logs/mmall.log.%d{yyyy-MM-dd}.gz</fileNamePattern>
<append>true</append>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
</encoder>
</appender>

<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--<File>d:/mmalllog/error.log</File>-->
<File>/developer/apache-tomcat-7.0.73/logs/error.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/devsoft/apache-tomcat-7.0.73/logs/error.log.%d{yyyy-MM-dd}.gz</fileNamePattern>
<!--<fileNamePattern>d:/mmalllog/error.log.%d{yyyy-MM-dd}.gz</fileNamePattern>-->
<append>true</append>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<logger name="com.mmall" additivity="false" level="INFO" >
<appender-ref ref="mmall" />
<appender-ref ref="console"/>
</logger>

<!-- geelynote mybatis log 日志 -->
<logger name="com.mmall.dao" level="DEBUG"/>

<!--<logger name="com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate" level="DEBUG" >-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->

<!--<logger name="java.sql.Connection" level="DEBUG">-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->
<!--<logger name="java.sql.Statement" level="DEBUG">-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->

<!--<logger name="java.sql.PreparedStatement" level="DEBUG">-->
<!--<appender-ref ref="console"/>-->
<!--</logger>-->

<root level="DEBUG">
<appender-ref ref="console"/>
<appender-ref ref="error"/>
</root>

</configuration>

具体用到再做补充。

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~