Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

Java Logger

2021/10/25 Java
Word count: 3,157 | Reading time: 15min

Java Logger——Java的日志体系

在JDK 1.3及以前,Java打日志依赖System.out.println(), System.err.println()或者e.printStackTrace(),Debug日志被写到STDOUT流,错误日志被写到STDERR流。这样打日志有一个非常大的缺陷,即无法定制化,且日志粒度不够细。

于是, Gülcü 于2001年发布了Log4j,后来成为Apache 基金会的顶级项目。Log4j 在设计上非常优秀,对后续的 Java Log 框架有长久而深远的影响,它定义的Logger、Appender、Level等概念如今已经被广泛使用。Log4j 的短板在于性能,在Logback 和 Log4j2 出来之后,Log4j的使用也减少了。

整个分类和发展历程如下:

![Java Logging](./Java-Logger/Java Logging.png)

其中,目前比较主流的使用方法和性能较高的组合是,SLF4J + Logback

SLF4J-Simple Logging Facade for Java

看英文全写可以知道,这个东东是Java中日志实现的一个简单接口,是对多种日志logging框架的一个抽象,如jul:java.util.logging、logback、log4j

大家在自己写项目的时候都会用到日志记录的功能,而通常下来,我们只记得想用的时候在类内协商var logger: Log = LogFactory.getLog(VerticalAlgorithm::class.java)--Koltinorprivate Log logger = LogFactory.getLog(VerticalAlgorithm.class)--Java,而等到自己写依赖的时候却不知道是咋回事,只知道Java的日志系统貌似挺复杂的。实际上,上面的代码是使用了SLF4J的规范,如果不正确引入依赖的话运行时会报如下的错误:

SLF4J: No SLF4J providers were found

然而检查依赖时,会发现已经引入slf4j-api了,然而还是报错了,这是为什么呢?其原因是,SLF4J本身不是一个日志库,而是一个日志库的抽象层,它必须依赖底层的日志库。

详情见SLF4J官网提供的图:

log-api-implement

两层: 抽象层+实现层

从图中可知,SLF4J必须和其他日志库配合才能正常运行。因此一般来说,需要将抽象层(例如slf4j-api-xx.jar)+中间层(例如slf4j-log4j12)+实现层(例如log4j)这三层都配置好才能保证SLF4J正常运行。

另外,有的日志库可以去掉中间层,例如slf4j-api和slf4j-simple就可以直接配合。

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

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.8.0-beta0</version>
</dependency>
<!-- -->注两个版本是一致的<!-- -->

这种方式就是抽象层+实现层的组合。使用这种方式只需要两个jar包

三层: 抽象层+实现层

按照图片所展示的,也有抽象层+中间层+实现层三层的实现方式,如下所示:

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>1.8.0-beta0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.8.0-beta0</version>
</dependency>

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

然而,直接使用的话还是会出现一点问题:No appenders could be found for logger(log4j)?

这其实是要添加一个配置文件log4j.properties声明appenders,内容如下(内容可以自定义):

1
2
3
4
5
6
7
8
9
# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, A1

# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender

# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

最佳实践

推荐使用 SLF4J + Logback。maven依赖如下,其中version字段用占位符代替,应该根据项目的实际情况选择合适的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.0.6</version>
</dependency>
-->
<!-- 该依赖包括了上面两个依赖,所以只要引入该依赖即可 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.6</version>
</dependency>

SLF4J适配

因为当时Java的日志组件比较混乱繁杂,Ceki Gülcü推出slf4j后,也相应为行业中各个主流日志组件推出了slf4j的适配。因此slf4j支持各种适配,无论你现在是用哪种日志组件,你都可以通过slf4j的适配器来使用上slf4j。只要你切换到了slf4j,那么再通过slf4j用上实现组件。给大家一个整体的依赖图(网上看到的)

slf4j适配

Logback

在代码中使用方式主要参考SLF4J API如何使用即可,Logback相关的主要是其配置文件logback.xml。

首先,如果依赖了logback,但在resources中没有创建logback.xml的配置文件,则默认输出到控制台且输出级别为trace以上。

1
2
3
14:59:54,588 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
14:59:54,589 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
14:59:54,589 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/F:/JavaCode/plainJava/target/classes/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
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">

<property name="LOG_HOME" value="tp/log"/>

<!-- 输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender" >
<!-- 输出的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}: %msg%n</pattern>
</encoder>
</appender>

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>F:\JavaCode\plainJava\src\main\resources\\test1.log</file>
<encoder>
<pattern>%date{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>

<appender name="RollAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 配置滚动的策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志名称的格式 -->
<fileNamePattern>${LOG_HOME}/logback.log.%d{yyyy-MM-dd}</fileNamePattern>
<!-- 保存的最长时间:天数 -->
<MaxHistory>1</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}: %msg%n</pattern>
</encoder>
</appender>

<!--
注意:
level属性也可以直接写在logger上,如:
<logger name="ws.log.logback.LogbackTest" additivity="false" level="INFO">
<appender-ref ref="STDOUT" />
</logger>
-->
<logger name="ws.log.logback.LogbackTest" additivity="false">
<level value="INFO" />
<appender-ref ref="STDOUT" />
</logger>

<!-- 相当于logger元素,只是name值已经确定为root了 -->
<root level="warn">
<appender-ref ref="STDOUT" />
</root>
</configuration>

讲解下RollingFileAppender, 这个是我们在日常日志系统里经常能看到的模式。由于日志数量会越来越多,所以如果都放在一个文件内的话,会导致文件大小变得很大,因此通过时间间隔分割将能更好地分割日志文件,也有助于之后日常地查询。关于RollingFileAppender其中File属性的设定如下:

设置File属性

  1. 系统会将日志内容全部写入log/check.log中。
  2. 在2019-06-05凌晨,check.log会被重命名为log/check.2019-06-04.log
  3. 然后再生成新的check.log文件,按照上面的步骤生成log/check.2019-06-05.loglog/check.2019-06-06.log等日志。

忽略File属性

  1. 系统会将日志内容直接写入log/check.2019-06-04.log中。
  2. 在2019-06-05凌晨,系统会将日志内容直接写入log/check.2019-06-04.log

即以2019-06-04为例,如果你设置了File属性,当天你只能看到check.log日志文件,2019-06-05才会看到check.201-06-04.log文件。但是如果你忽略了,你当天就能看到check.2019-06-04.log文件,但你始终看不到check.log文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<appender name="emergencyLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 写入日志内容的文件名称(目录) -->
<File>log/check.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<fileNamePattern>log/check.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<!-- pattern节点,用来设置日志的输入格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger [%msg]%n</pattern>
<!-- 记录日志的编码:此处设置字符集 - -->
<charset>UTF-8</charset>
</encoder>
</appender>

以上述配置执行的话,会在工程根目录即src、.idea那层生成log文件夹,其中包含日志log。即相对路径是以根目录为起点的

附录

  • (J)CL使用方式
1
2
3
4
5
6
import org.apache.commons.logging.Log
import org.apache.commons.logging.

object VerticalAlgorithm : RemappingAlgorithm {
var logger: Log = LogFactory.getLog(VerticalAlgorithm::class.java)
}
  • slf4j使用方式
1
2
3
4
5
6
7
8
9
10
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogTest {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(LogTest.class);
logger.info("ues");
}
}
// 或者使用lombok的@Slf4j标注类上功能时一样的

再提几点最佳实践指导原则:

  • 总是使用 Log Facade,而不是具体的 Log Implementation

  • 只添加一个 Log Implementation 依赖

  • 具体的日志依赖应该设置为 optional,并使用 runtime scope

    ​ 设为optional,依赖不会传递,这样如果你是个lib项目,然后别的项目使用了你这个lib,不会被引入不想要的Log Implementation 依赖;
    Scope设置为runtime,是为了防止开发人员在项目中直接使用Log Implementation中的类,而不使用Log Facade中的类。

  • 如果有必要, 排除依赖的第三方库中的Log Impementation依赖

    ​ 这是很常见的一个问题,第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为optional,然后你的项目继承了这些依赖——具体的日志实现未必是你想使用的,比如他依赖了Log4j,你想使用Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和Log实现,也极容易形成环。
    这种情况下,推荐的处理方法,是使用exclude来排除所有的这些Log实现和桥接器的依赖,只保留第三方库里面对Log Facade的依赖。

阿里日志规范

阿里对此的代码规范:

【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(Abc.class);

让Spring统一输出

这就是为了对slf4j的适配做一个例子说明。Spring是用JCL作为日志门面的,那我们的应用是slf4j + logback,怎么让Spring也用到logback作为日志输出呢?这样的好处就是我们可以统一项目内的其他模块、框架的日志输出(日志格式,日志文件,存放路径等,以及其他slf4j支持的功能) 很简单,就是加入jcl-over-slf4j.jar就好了。

spring-log

日志配置启动过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
14:59:54,588 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
14:59:54,589 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]

★14:59:54,589 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/F:/JavaCode/plainJava/target/classes/logback.xml]

14:59:54,835 |-INFO in ch.qos.logback.core.joran.action.ImplicitModelAction - Assuming default class name [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for tag [encoder]
14:59:54,836 |-INFO in ch.qos.logback.core.joran.action.ImplicitModelAction - Assuming default class name [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for tag [encoder]
14:59:54,952 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - Processing appender named [STDOUT]
14:59:54,952 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]

★★14:59:55,111 |-ERROR in ch.qos.logback.core.pattern.parser.Compiler@737996a0 - There is no conversion class registered for conversion word [thead]
★★14:59:55,111 |-ERROR in ch.qos.logback.core.pattern.parser.Compiler@737996a0 - [thead] is not a valid conversion word

14:59:55,220 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - Processing appender named [FILE]
14:59:55,220 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - About to instantiate appender of type [ch.qos.logback.core.FileAppender]

★★14:59:55,224 |-ERROR in ch.qos.logback.core.pattern.parser.Compiler@61dc03ce - There is no conversion class registered for conversion word [thead]
★★14:59:55,224 |-ERROR in ch.qos.logback.core.pattern.parser.Compiler@61dc03ce - [thead] is not a valid conversion word

14:59:55,224 |-INFO in ch.qos.logback.core.FileAppender[FILE] - File property is set to [./test.log]

★14:59:55,226 |-INFO in ch.qos.logback.classic.model.processor.RootLoggerModelHandler - Setting level of ROOT logger to WARN
★14:59:55,228 |-INFO in ch.qos.logback.core.model.processor.AppenderRefModelHandler - Attaching appender named [STDOUT] to Logger[ROOT]
★14:59:55,228 |-INFO in ch.qos.logback.core.model.processor.AppenderRefModelHandler - Attaching appender named [FILE] to Logger[ROOT]

[]

Author: Mrli

Link: https://nymrli.top/2021/10/20/Java-Logger/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
CICD-Jenkins与Travis
NextPost >
javadoc——让大家更好写java项目
CATALOG
  1. 1. Java Logger——Java的日志体系
    1. 1.1. SLF4J-Simple Logging Facade for Java
      1. 1.1.1. 两层: 抽象层+实现层
      2. 1.1.2. 三层: 抽象层+实现层
      3. 1.1.3. 最佳实践
      4. 1.1.4. SLF4J适配
    2. 1.2. Logback
  2. 2. 附录
    1. 2.1. 阿里日志规范
    2. 2.2. 让Spring统一输出
    3. 2.3. 日志配置启动过程