目录

  1. 日志概述
    1. 日志体系概述
  2. 混乱的日志体系
    1. 上古时代
    2. 开创先驱
    3. 搞事情的 JUL
    4. JCL 应运而生
    5. 再起波澜
    6. 再度青春
  3. 配置讲解
    1. 日志级别
    2. 日志组件
    3. JUL
    4. log4j
    5. logback
      1. logback 组件之间的关系
      2. 基本配置信息
    6. log4j2
    7. jcl
    8. slf4j
  4. Spring Boot 中的日志使用
  5. 日志使用建议
    1. 门面约束
    2. 单一原则
    3. 依赖约束
    4. 避免传递
    5. 注意写法
    6. 减少分析
    7. 精简至上
  6. 附录

日志概述

❓为什么需要日志?

在很多的情况下,日志可能是我们了解应用如何执行的唯一方式,所以学习日志非常有必要。更何况发展到今天的软件系统已经非常复杂,特别时服务器的软件,设计到的知识和内容问题非常的多,有了日志才能够更方便了解系统运行过程中出现的种种问题。通过日志可以处理历史数据、诊断问题的追踪以及理解系统的活动。

根据日志记录的内容不通,可以人为的将日志分为两种类型的日志:调试日志系统日志

在软件开发中,需要经常的调试程序,或者做一些状态的输出,便于我们查询程序的运行状况,为了让我们能够更加灵活且方便的控制这些调试信息,我们肯定是需要更加专业的日志技术。我们平时在调试程序的过程中所使用的肯定就是专业开发工具自带的 debug 功能,可以实时查看程序运行情况,不能够有效保存运行情况的信息。调试日志是能够更加方便的去「 重现 」这些问题

系统日志是实际生产环境中最常使用的日志,经常用来记录系统中硬件、软件和系统相关问题的信息,同时还可以监视系统中发生的事件,用户可以通过它来检查错误发生的原因,或者寻找到攻击时留下的痕迹。系统日志包括系统运行日志、应用日志和安全日志这几种

日志体系概述

在 Java 中一直秉持着面向接口编程,所以任何优秀的框架一定是先存在接口,而后再是根据这些接口做到不同的实现,Java 日志体系如下图:

Java 中相关的日志门面系统概述如下:

接口含义
JCLJava Commons Loggin,Apache 基⾦会所属的项⽬,是⼀套 Java ⽇志接⼝
SLF4JSimple Logging Facade for Java,是一套简单的 Java 日志门面,只提供相关接口,和其他日志之间需要桥接

门面其实就是表示,这是一套接口,并没有给出 Java 日志的具体实现方式,其中所说的桥接,指的是桥接模式

❓为什么要使用日志门面技术

每一种日志框架都有自己单独的 API,要使用对应的框架就要使用对应的 API,这就大大的增加了应用程序代码对于日志框架的耦合性。使用日志门面技术之后,对于应用程序来说,无论底层的日志框架如何改变,应用程序不需要修改任何一行代码就可以上线运行

Java 中相关的日志实现概述如下:

日志实现含义
JULJDK 中的⽇志⼯具
Log4j⾪属于 Apache 基⾦会的⼀套⽇志框架,现已不再维护
Log4j2Log4j 的升级版本,与 Log4j 变化很⼤,不兼容
Logback⼀个具体的⽇志实现框架,和 Slf4j 是同⼀个作者,性能很好

混乱的日志体系

Java 的日志体系非常的混乱,本小节将主要阐述 Java 日志体系的发展过程

上古时代

在 JDK 1.3 及以前,Java 打⽇志依赖 System.out.println(), System.err.println() 或者 e.printStackTrace(),Debug ⽇志被写到 STDOUT 流,错误⽇志被写到 STDERR 流。这样打⽇志有⼀个⾮常⼤的缺陷:日志记录⾮常机械,⽆法定制,且⽇志粒度不够细分

开创先驱

随后,在 2001 年发布了 Log4j,Log4j 在设计上⾮常优秀, 它定义的 Logger、Appender、Level 等概念对后续的 Java Log 框架有深远的影响,如今的很多⽇志框架基本沿⽤了这种思想。Log4j 的性能是个问题,在 Logback 和 Log4j2出来之后,2015 年 9 ⽉, Apache 软件基⾦会宣布,Log4j 不再维护,建议所有相关项⽬升级到 Log4j2
✨ 如果要在项目中使用 log4j,只需要两步就可以了,分别是:引入 log4j 依赖以及配置 log4j

引入 log4j 依赖:

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

日志输出模式配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
log4j.rootLogger=debug
#console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=log4j:[%d{yyyy-MM-dd HH:mm:ssa}]:%p %l%m%n
#dailyfile
log4j.appender.dailyfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.dailyfile.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.dailyfile.File=./log4j.log
log4j.appender.dailyfile.Append=true
log4j.appender.dailyfile.Threshold=INFO
log4j.appender.dailyfile.layout=org.apache.log4j.PatternLayout
log4j.appender.dailyfile.layout.ConversionPattern=log4j:[%d{yyyy-MM-dd HH:mm:ssa}] [Thread: %t][ Class:%c >> Method: %l ]%n%p:%m%n

搞事情的 JUL

sun 公司对于 log4j 的出现内⼼心隐隐表示嫉妒。于是在 jdk1.4 版本后,开始搞事情,增加了一个日志包(java.util.logging),简称为 JUL,⽤以对抗 log4j ,但是却给开发造成了麻烦,这是因为 JUL 和 log4j 的 API 并不统一,使得项目中日志难以管理。

✨ 使用 JUL 非常的简单,由于是 JDK 中自带的所以并不需要引入额外的依赖,只需要配置即可,默认的配置路径是:$JAVA_HOME/jre/lib/logging.properties

1
2
Logger loggger = Logger.getLogger(Demo.class.getName());
logger.finest("xxxx");

JUL 功能远不如 log4j 完善,⾃带的 Handlers 有限,性能和可用性上也一般,不是非常推荐在项目中使用

JCL 应运而生

由于 JUL 的 api 与 log4j 是完全不同的,并且日志系统互相没有关联,彼此没有约定,不同人的代码使⽤不同日志,替换和统一也就变成了了⽐较棘手的⼀件事。解决这个问题的答案就是进行抽象抽象出一个接口层,对每个日志实现都适配或者转接,这样这些提供给别⼈的库都直接使用抽象层即可 ,以后调用的时候,就调用这些接⼝

JCL(Jakarta Commons Logging)应运而生,也就是 commons-logging-xx.jar 组件。JCL 只提供 log 接口,具体的实现则在运行时动态寻找。用户可以自由选择第三方的日志组件作为具体实现,像 log4j,或者 jdk 自带的 jul, common-logging 会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。

common-logging 内部有一个 Simple logger 的简单实现,但是功能很弱。所以使用 common-logging,通常都是配合着 log4j 以及其他日志框架来使用,用它的好处就是,代码依赖是 common-logging 而非 log4j 的 API, 避免了和具体的日志 API 直接耦合,在有必要时,可以更改日志实现的第三方库。

JCL 会在 ClassLoader 中进⾏查找,如果能找到 Log4j 则默认使⽤ log4j 实现,如果没有则使⽤ JUL(jdk ⾃带的) 实现,再没有则使⽤ JCL 内部提供的 SimpleLog 实现,如下图:

如果需要在项目中使用 JCL 同样分为两步:引入依赖以及配置

引入依赖

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

测试代码

1
2
3
4
5
6
public class TestLoger {
public static void main(String[] args) {
Log log = LogFactory.getLog(TestLoger.class);
log.info("xxxxxxx");
}
}

JCL 缺点也很明显,⼀是效率较低,二是容易引发混乱,三是 JCL 机制有很⼤的可能会引发内存泄露。同时,JCL 的书写存在⼀个不太优雅的地⽅,典型的场景如下:假如要输出一条 debug 日志,而一般情况下,生产环境 log 级别都会设到 info 或者以上,那这条 log 是不不会被输出的。于是,在代码里就出现了

1
logger.debug("this is a debug info , message :" + msg);

这个有什么问题呢?虽然⽣产不会打出⽇志,但是这其中都会做⼀个字符串连接操作,然后⽣成⼀个新的字符串。如果这条语句在循环或者被调⽤很多次的函数中,就会多做很多⽆⽤的字符串连接非常影响性能,所以,JCL 推荐的写法如下。这样虽然解决了问题,但是这个代码实在看上去不怎么舒服

1
2
3
if (logger.isDebugEnabled()) {
logger.debug("this is a debug info , message :" + msg);
}

再起波澜

于是,针对以上情况,log4j 的作者再次出⼿手,他觉得 JCL 不好用,⾃己⼜写了一个新的接⼝ api,就 是 slf4j,并且为了追求更极致的性能,新增了一套日志的实现,就是 logback

引入 slf4j 依赖

pom.xml
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>1.7.30</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

logback-core 提供基础抽象,logback-classic 提供日志实现,并且直接就是基于 Slf4j API。所以 slf4j 配合 logback 来完成日志时,不需要像其他的日志框架一样提供适配器器

slf4j 本身并没有实际的日志输出能力,它底层还是需要去调⽤用具体的日志框架 API,也就是它需要跟具体的⽇志框架结合使用。由于具体日志框架⽐较多,而且互相也大都不兼容,日志⻔⾯接口要想实现 与任意⽇日志框架结合就需要额外对应的桥接器器

slf4j 的配置

slf4j.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<property name="logPattern" value="logback:[ %-5level] [%date{yyyy-MM-dd
HH:mm:ss.SSS}] %logger{96} [%line] [%thread]- %msg%n"></property>
<!-- 控制台的标准输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>UTF-8</charset>
<pattern>${logPattern}</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

有了新的 slf4j 后,上⾯字符串的拼接问题,被以下代码所取代,⽽ logback 也提供了更⾼级的特性,如:异步 logger,Filter 等

1
logger.debug("this is a debug info, message :{}",msg);

再度青春

前⾯提到,log4j 由 apache 宣布,2015 年后,不再维护。推荐大家升级到 log4j2,虽然 log4j2 沿袭了 log4j 的思想,然而 log4j2 和 log4j 完全是两码事,并不不兼容。log4j2 以性能著称,它比其前身 Log4j 1.x 提供了重⼤大改进,同时类比 logback,它提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些固有问题。功能上,它有着和 Logback 相同的基本操作,同时又有⾃己独特的部分,⽐如:插件式结构、配置⽂文件优化、异步⽇日志等。

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.1</version>
</dependency>
1
2
Logger logger = LogManager.getLogger(Demo.class);
logger.debug("debuff Msg");
log4j2.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="info" monitorInterval="30">
<Properties>
<Property name="pattern">log4j2:[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t]
%c{1}:%L - %msg%n</Property>
</Properties>
<appenders>
<!--console :控制台输出的配置-->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${pattern}"/>
</Console>
</appenders>
<loggers>
<logger name="org.springframework" level="INFO"></logger>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>

但是最近出现的 log4j2 安全隐患问题,让我有点怀疑是否要进一步使用这个日志框架

配置讲解

众多 Java 日志的使用并不是日志框架的难点,重点在于:如何使用配置文件,定制化日志

日志级别

⼀个完整的⽇志组件都要具备⽇志级别的概念,每种⽇志组件级别定义不同,⽇常编码最经常⽤到的主流分级如下(由低到⾼)

日志级别含义
trace路径跟踪
debug⼀般⽤于⽇常调式
info打印重要信息
warn给出警告
error出现错误或问题

每个日志组件的具体级别划分稍有不同,需要具体分析

级别级别含义
SEVERE错误——最高的日志级别
WARNING警告
INFO(默认级别)信息
CONFIG配置
FINE详细信息(少)
FINER详细信息(中)
FINEST详细信息(多)——最低级的日志级别

两个特殊的级别

级别含义
OFF可用来关闭日志记录
ALL启用所有消息的日志记录

对于日志的级别,重点关注的是 new 对象的时候的第二个参数,是一个数值

1
public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);
级别数值
OFFInteger.MAX_VALUE
SEVERE1000
ALLInteger.MIN_VALUE

这个数值的意义在于,如果设置的日志级别是 INFO-800,最终展现的日志信息,必须是数值大于 800 的所有的日志信息,最终展现的就是:SERVER,WARNING,INFO

1
2
Logger logger = Logger.getLogger("com.pineappltman.test.JulLearning");
logger.setLevel(Level.CONFIG);

如果仅仅只是通过以下形式来设置日志级别,那么不能起到效果,将来需要搭配处理器 handler 共同设置才会生效

日志组件

日志组件含义
appender⽇志输出⽬的地,负责⽇志的输出 (输出到什么 地⽅)
logger⽇志记录器,负责收集处理⽇志记录 (如何处理⽇志)
layout⽇志格式化,负责对输出的⽇志格式化(以什么形式展现)

JUL

默认情况下配置文件的路径为$JAVAHOME\jre\lib\logging.properties,可以通过编码的方式指定配置文件

1
2
3
4
5
6
static {
System.setProperty("java.util.logging.config.file",
Demo.class.getClassLoader().getResource("logging.properties")
.getPath()
);
}

JUL 中的级别有:SEVER(最高值)、WARNING、INFO、CONFIG、FINE、FINER、FINEST(最小值)、OFF(关闭日志)、ALL(启动所有日志)
JUL 中的处理器如下:

处理器含义
StreamHandler⽇志记录写⼊ OutputStream
ConsoleHandler⽇志记录写⼊ System.err
FileHandler日志记录写入单个文件或一组滚动日志文件
SocketHandler⽇志记录写⼊远程 TCP 端口的处理程序
MemoryHandler缓冲内存中⽇日志记录

格式化

格式化含义
SimpleFormatter格式化为简短的⽇日志记录摘要
XMLFormatter格式化为详细的 XML 结构信息
输出格式可以自定义输出,只要继承自抽象类 java.util.logging.Formatter 即可

log4j

启动时,默认会寻找 resorce folder 下的 log4j.xml,若没有,会寻找 lo4g.properties

Loggers(日志记录器),Appenders(输出控制器)和 Layout(日志格式化器)组成。其中 Loggers 控制日志的输出以及输出级别(JUL 做日志级别 Level);Appenders 指定日志的输出方式(输出到控制台、文件等);Layout 控制日志信息的输出格式

logback

Logback 是由 log4j 创始人设计的另一个开源日志组件,性能比 log4j 要好,Logback 主要分为三个模块:

  • logback-core:其他两个模块的基础模块
  • logback-classic:它是 log4j 的一个改良版本,同时它完整实现了 slf4j API
  • logback-access:访问模块与 Servlet 容器集成提供通过 Http 来访问日志的功能
    pom.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <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.2.3</version>
    </dependency>
    LogBackTest.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //定义日志对象
    public final static Logger LOGGER = LoggerFactory.getLogger(LogBackTest.class);
    @Test
    public void testSlf4j(){ //打印日志信息
    LOGGER.error("error");
    LOGGER.warn("warn");
    LOGGER.info("info");
    LOGGER.debug("debug");
    LOGGER.trace("trace");
    }

logback 会依次读取一下类型配置文件: logback.groovy,logback-test.xmllogback.xml如果均不存在,会采用默认的配置

logback 组件之间的关系

  1. Logger:日志的记录器,把它关联到应用的对应的 context 上后,主要用于存放日志对象,也可以定义日志类型、级别
  2. Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等
  3. Layout:负责把事件转换成字符串,格式化的日志信息的输出。在 logback 中 Layout 对象被封装在 encoder 中

基本配置信息

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--格式化输出:%d表示日期,%thread表示线程名,
%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]%-5level %msg%n"/>
<!--
Appender: 设置日志信息的去向,常用的有以下几个:
ch.qos.logback.core.ConsoleAppender (控制台)
ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
ch.qos.logback.core.FileAppender (文件)
-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--输出流对象 默认 System.out 改为 System.err-->
<target>System.err</target>
<!--日志格式配置-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!-- 用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
<loger>仅有一个name属性,一个可选的level和一个可选的addtivity属性
name:用来指定受此 logger 约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,如果未设置此属性,那么当前logger将会继承上级的级别
additivity:是否向上级loger传递打印信息。默认是true
<logger>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger -->

<!--它是根 logger 元素,默认debug level-->
<root level="ALL">
<appender-ref ref="console"/>
</root>
</configuration>

log4j2

  • Log4j will inspect the log4j.configurationFile system property and, if set, will attempt to load the configuration using the ConfigurationFactory that matches the file extension.
  • If no system property is set the YAML ConfigurationFactory will look for log4j2-test.yaml or log4j2-test.yml in the classpath
  • If no such file is found the JSON ConfigurationFactory will look for log4j2-test.json or log4j2-test.jsn in the classpath
  • If no such file is found the XML ConfigurationFactory will look for log4j2-test.xml in the classpath.
  • If a test file cannot be located the YAML ConfigurationFactory will look for log4j2.yaml or log4j2.yml on the classpath
  • If a YAML file cannot be located the JSON ConfigurationFactory will look for log4j2.json or log4j2.jsn on the classpath.
  • If a JSON file cannot be located the XML ConfigurationFactory will try to locate log4j2.xml on the classpath
  • If no configuration file could be located the DefaultConfiguration will be used. This will cause logging output to go to the console.

log4j2 的级别从低到高是:

1
ALL -> TRACE -> DEBUG -> INFO -> WARN -> ERROR -> FATAL -> OFF

相关处理器,官方介绍

处理器含义
FileAppender输出到本地⽂件
KafkaAppender输出到 kafka 队列
FlumeAppender将几个不同源的日志汇集、集中到⼀处
JMSQueueAppender/JMSTopicAppender与 JMS 相关的日志输出
RewriteAppender对日志事件进⾏掩码或注⼊信息
RollingFileAppender对日志文件进行封存
RoutingAppender在输出地之间进行筛选路由
SMTPAppender将 LogEvent 发送到指定邮件列表
SocketAppender将 LogEvent 以普通格式发送到远程主机
SyslogAppender将 LogEvent 以 RPF5424 格式发送到远程主机
AsynchAppender将一个 LogEvent 异步地写入多个不同输出地
ConsoleAppender将 LogEvent 输出到命令行
FailoverAppender维护一个队列,系统尝试向队列中的 Appender 依次输出 LogEvent,直到有一个成功为止
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
 <?xml version="1.0" encoding="UTF-8"?>
<!--⽇志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--status="WARN" :⽤于设置log4j2⾃身内部⽇志的信息输出级别,默认是OFF-->
<!--monitorInterval="30" :间隔秒数,自动检测配置⽂件的变更和重新配置本身-->
<configuration status="info" monitorInterval="30">
<Properties>
<!--⾃定义一些常量,之后使用${变量名}引用-->
<Property name="pattern">log4j2:[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t]%c{1}:%L - %msg%n</Property>
</Properties>
<!--appenders:定义输出内容,输出格式,输出⽅式,日志保存策略略等,常用下三种标签 [console,File,RollingFile]-->
<appenders>
<!--console :控制台输出的配置-->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${pattern}"/>
</Console>
<!--File :同步输出⽇志到本地文件-->
<!--append="false" :根据其下⽇志策略,每次清空⽂件重新输⼊日志,可用于测试-->
<File name="File" fileName="./log4j2-file.log" append="false">
<PatternLayout pattern="${pattern}"/>
</File>
<RollingFile name="RollingFile" fileName="./log4j2-rollingfile.log"
filePattern="./$${date:yyyy-MM}/log4j2-%d{yyyy-MM-dd}-%i.log">
<!--ThresholdFilter :日志输出过滤-->
<!--level="info" :⽇志级别,onMatch="ACCEPT" :级别在info之上则接受,onMismatch="DENY" :级别在info之下则拒绝-->
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${pattern}"/>
<!-- Policies :⽇志滚动策略-->
<Policies>
<!-- TimeBasedTriggeringPolicy :时间滚动策略略, 默认0点产⽣新的文件,interval="6"
: ⾃定义文件滚动时间隔,每隔6⼩时产生新文件, modulate="true"
: 产⽣文件是否以0点偏移时间,即6点,12点,18点,0点-->
<TimeBasedTriggeringPolicy interval="6" modulate="true"/>
<!-- SizeBasedTriggeringPolicy :文件大小滚动策略-->
<SizeBasedTriggeringPolicy size="1 MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个⽂件,这里设置了20 -->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</appenders>
<!--然后定义logger,只有定义了了logger并引⼊入的appender,appender才会⽣生效--> <loggers>
<!--过滤掉spring和mybatis的⼀一些⽆无⽤用的DEBUG信息-->
<!--Logger节点⽤来单独指定日志的形式,name为包路径,⽐如要为org.springframework 包下所有⽇志指定为INFO级别等。 ->
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<!--AsyncLogger :异步⽇志,LOG4J 有三种⽇志模式,全异步日志,混合模式,同步日志,性能从高到低,线程越多效率越高,也可以避免日志卡死线程情况发生-->
<!--additivity="false" : additivity设置事件是否在root logger输出,为了避免重复输出,可以在Logger 标签下设置additivity为”false”-->
<AsyncLogger name="AsyncLogger" level="trace" includeLocation="true"additivity="true">
<appender-ref ref="Console"/>
</AsyncLogger>
<logger name="Kafka" additivity="false" level="debug">
<appender-ref ref="Kafka"/>
<appender-ref ref="Console"/>
</logger>
<!-- Root节点⽤来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该 Root ⽇志输出 -->
<root level="info">
<appender-ref ref="Console"/>
<!--<appender-ref ref="File"/>-->
<!--<appender-ref ref="RollingFile"/>-->
<!--<appender-ref ref="Kafka"/>-->
</root>
</loggers>
</configuration>

jcl

配置文件,首先在 classpath 下寻找 commons-logging.properties 文件。如果找到,则使⽤其中定义的 Log 实现类;如果找不到,则再查找是否已定义系统环境变量 org.apache.commons.logging.Log,找到则使⽤其定义的 Log 实现类

查看 classpath 中是否有 Log4j 的包,如果发现则⾃动使⽤ Log4j 作为⽇志实现类;否则,使用 JDK 自身的日志实现类(JDK 1.4 以后才有日志实现类);否则,使用 commons-logging 自己提供的一个简单的日志实现类 SimpleLog

JCL 有五个级别:

1
trace -> debug -> info -> warn -> error
1
2
3
4
5
6
#指定⽇日志对象:
#org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
#org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger
#指定⽇志⼯厂:
org.apache.commons.logging.LogFactory = org.apache.commons.logging.impl.LogFactoryImpl

slf4j

具体日志输出内容取决于具体的日志实现

slf4j 日志级别从高到低有五种:ERROR、WARN、INFO、DEBUG、TRACE

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
 <!--slf转其他日志-->
<!--slf - jcl-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jcl</artifactId>
<version>1.7.30</version>
</dependency>
<!-- slf - log4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
<!-- slf4j - log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.13.0</version>
</dependency>
<!--slf - jul-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.30</version>
</dependency>
<!--slf - simplelog-->
<dependency>

<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
<!--slf - logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

如果存在多个桥接器的话,slf4j 会使用在 classpath 中较早出现的那个,如在 maven 中,会使用在 pom.xml 中定义较靠前的桥接器

不仅 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
<!--其他⽇日志转slf-->
<!--jul - slf-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.30</version>
</dependency>

<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.30</version>
</dependency>
<!--log4j - slf-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.30</version>
</dependency>
<!--log4j2 - slf-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.13.0</version>
</dependency>

日志之间的桥接,非常容易出现 slf4j 的一个著名问题:slf4j 的日志环问题

Spring Boot 中的日志使用

springboot 框架在企业中的使用越来越普遍,spring boot 日志也是开发中常用的日志系统。spring boot 默认就是使用 SLF4J 作为日志门面,logback 作为日志实现来记录日志
引入如下依赖:

pom.xml
1
2
3
4
<dependency>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>

测试代码如下:

SpringbootLogApplicationTests.java
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class SpringbootLogApplicationTests{
public static final Logger LOGGER = LoggerFactory.getLogger(SpringBootLogApplicationTests.class);
@Test
public void contextLoads(){
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info"); // 默认日志级别
LOGGER.debug("debug");
LOGGER.trace("trace");
}
}

默认的日志配置如下:

application.yml
1
2
3
4
5
6
7
8
9
10
logging.level.com.pineapple=trace

# 在控制台输出的日志的格式 同 logback

logging.pattern.console=%d{yyyy-MM-dd} [%thread] [%-5level] %logger{50} -%msg%n

# 指定文件中日志输出的格式

logging.file=D:/logs/springboot.log
logging.pattern.file=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n

如果给类路径下放上每个日志框架自己的配置文件,Spring Boot 就不使用默认的配置了

日志框架配置文件
Logbacklogback-spring.xml,locback.xml
log4j2log4j2-spring.xml,log4j2.xml
jullogging.properties

日志使用建议

既然 Java 有这么多日志,那么到底应该如何优雅的使用 Java 日志?

门面约束

使用门面,而不是具体实现,使⽤ Log Facade 可以方便的切换具体的日志实现。⽽且,如果依赖多个项目,使⽤了不同的 Log Facade,还可以方便的通过 Adapter 转接到同⼀个实现上。

👴 日志门面现在推荐使用 Log4j-API 或者 SLF4j,不推荐继续使用 JCL

单一原则

只添加一个日志实现,项目中应该只使用一个具体的 Log Implementation,如果在依赖的项目中,使用的 Log Facade 不支持当前 Log Implementation,就添加合适的桥接器

👴 JUL 性能一般,log4j 性能也有问题而且不再维护,目前 log4j2 还有安全问题,推荐使用 logback

依赖约束

在项⽬中,Log Implementation 的依赖强烈建议设置为 runtime scope,并且设置为 optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 maven 添加依赖的时候这样设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
  • 设为 optional,依赖不会传递,这样如果是个 lib 项目,然后别的项⽬使用了这个 lib,不会被引入不想要的 Log Implementation 依赖;
  • Scope 设置为 runtime,是为了防止开发⼈员在项⽬中直接使用 Log Implementation 中的类,强制约束开发⼈员使用 Facade 接⼝

避免传递

尽量用 exclusion 排除依赖的第三方库中的⽇志坐标,同上⼀个话题,第三方库的开发者却未必会把具体的⽇志实现或者桥接器的依赖设置为 optional, 然后你的项⽬就会被迫传递引⼊这些依赖,而这些⽇志实现未必是你想要的,⽐如他依赖了 Log4j,你想使用 Logback,这时就很尴尬。另外,如果不同的第三⽅依赖使⽤了不同的桥接器和 Log 实现,极有可能会形成环。

这种情况下,推荐的处理⽅法,是使用 exclude 来排除所有的这些 Log 实现和桥接器的依赖,只保留第三⽅库⾥面对 Log Facade 的依赖

依赖 jstorm 会引⼊ Logback 和 log4j-over-slf4j,如果你在⾃己的项⽬中使用 Log4j 或其他 Log 实现的话,就需要加上 exclusion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<dependency>
<groupId>com.alibaba.jstorm</groupId>
<artifactId>jstorm-core</artifactId>
<version>2.1.1</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>

注意写法

避免为不会输出的 log 买单。Log 库都可以灵活的设置输出级别,所以每一条程序中的 log,都是有可能不会被输出的,这时候要注意不要额外的付出代价。

1
2
logger.debug("this is debug: " + message);
logger.debug("this is json msg: {}", toJson(message));

第一条的字符串拼接,即使日志级别⾼于 debug 不会打印,依然会做字符串连接操作; 第二条虽然⽤了 SLF4J/Log4j2 中的懒求值⽅式,但是 toJson()这个函数却是总会被调用并且开销更大。 推荐的写法如下:

1
2
3
4
5
6
7
8
 // SLF4J/LOG4J2
logger.debug("this is debug:{}", message);
// LOG4J2
logger.debug("this is json msg: {}", () -> toJson(message));
// SLF4J/LOG4J2
if (logger.isDebugEnabled()) {
logger.debug("this is debug: " + message);
}

减少分析

输出的日志中尽量不要使用行号,函数名等信息。原因是:为了获取语句所在的函数名,或者行号,log 库的实现都是获取当前的 stacktrace ,然后分析取出这些信息,而获取 stacktrace 的代价是昂贵的。如果有很多的日志输出,就会占用大量的 CPU,在没有特殊需求的情况下,建议不要在日志中输出这些字段

精简至上

log 中尽量不要输出稀奇古怪的字符,这是个习惯和约束问题,下面这个语句最好不要出现在项目中

1
logger.debug("===========================:{}",message);

输出大量无关字符,虽然自己一时痛快,但是如果所有人都这样做的话,那 log 输出就没法看了!正确的做法是日志只输出必要的信息,如果要过滤,后期使用 grep 来筛选,只查看自己关心的日志

附录

复杂 Spring 项目中 SLF4J 最佳使用姿势