日志框架简介
日志:用于记录程序中发生的事件,方便排查问题
日志框架:程序在运行过程中输出日志的一套解决方案,包括日志的级别、内容格式、日志文件的创建方案等
java中的日志框架可以大致分为两类:
- 日志门面:负责提供接口,旨在为不同的日志框架提供统一的接口,方便用户使用
- 日志实现:实现了日志门面,完成输出日志的功能。
常见的日志框架:
- 常见的日志门面:jcl、slf4j
- 常见的日志实现:jul、log4j、logback、log4j2
java的日志框架的发展历史
第一阶段: 在早期,jdk1.0到jdk1.3之间,没有官方的日志库,开发者只能使用System.out.println把信息打印到控制台,这种方式下,日志无法持久化,也难以分类过滤,是典型的"刀耕火种"时代
第二阶段: 2001 到 2005,为了解决只能直接向控制台输出日志的痛点,各种日志框架应运而生,包括 log4j 1.x,瑞士程序员开发,后来成为apache的顶级项目; jul,sun公司在jdk 1.4中提供的日志工具库; jcl,apache推出的,为log4j、jul提供统一的接口,不过使用体验并不理想。
第三阶段: 2005 到 2014, 2005年, log4j的作者,推出了日志门面框架 slf4j,它只提供接口,不包含实现,是后来最常用的日志门面框架; 2006年,log4j的作者推出了logback,相较于log4j,它的性能更好、内存占用更少,并且原生实现了slf4j
第四阶段: 2014 至今,性能之争。 log4j2.x的崛起,它也是log4j的作者开发的,并且借鉴了logback的优点,还引入了disruptor无锁队列,性能远超logback。
日志框架的最佳实践: 代码层面直接面向slf4j编程,logback因为是springboot的默认日志框架,是大多数应用的最佳选择,如果追求极致的性能,可以切换到log4j2
常用的日志框架
slf4j
slf4j:simple logging facade for java,一款日志门面框架。 slf4j中只有接口,没有实现,这里使用slf4j提供的最简单的实现 slf4j-simple 来作为演示。
使用案例
第一步:添加依赖
xml
<dependencies>
<!--slf4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--slf4j的简单实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
第二步:测试
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Slf4jDemo1 {
private static final Logger log = LoggerFactory.getLogger(Slf4jDemo1.class);
public static void main(String[] args) {
log.info("Hello World");
}
}
****
打印出的日志
text
[main] INFO org.example.Slf4jDemo1 - Hello World
总结
slf4j是用到的比较多的框架,它的特点是使用"{}"作为占位符,让日志中的字符串拼接更加简洁。
基本使用
为slf4j提供日志实现框架: slf4j要求有且仅有一个日志实现框架
- 添加slf4j-api的依赖,使用slf4j的API在项目中进行统一的日志记录
- 绑定具体的日志实现框架:
- 绑定已经实现slf4j的日志框架,直接添加对应的依赖,
- 绑定没有实现slf4j的日志框架,先添加日志的适配器,例如log4j的适配器的maven项目名称叫做slf4j-log4j12,再添加实现类的依赖。
slf4j的API:
- 获取日志记录器:
private static final Logger logger = LoggerFactory.getLogger(Slf4jTest.class); - 记录日志:调用info、debug等方法,进行日志记录
- 记录日志时,允许通过占位符
{}来完成字符串拼接
log4j
简介
apache旗下的一款开源日志框架,是日志实现框架
log4j中的组件:
- 日志记录器:Logger,负责收集处理日志记录
- 追加器:Appender,控制日志输出的位置
- 布局器:Layout,用于控制日志输出内容的格式
工作机制:
- 日志记录器由程序调用,用来记录日志信息,
- 追加器是绑定到日志记录器上面的,负责日志输出的位置
- 布局器是绑定到追加器上面的,负责日志内容的格式
常见的追加器:
- 把日志信息打印到控制台:ConsoleAppender
- 把日志信息打印到文件中:FileAppender
- 每天生成日志文件:DailyRollingFileAppender
- 依据文件大小来生成日志文件:RollingFileAppender
常见的布局器:
- 把日志内容输出为html格式的文件:HTMLLayout
- 使用指定的格式符来配置输出信息:PatternLayout
日志级别:由低到高,越低,记录的信息越详细
- trace:程序的追踪信息,记录程序所有的流程信息
- debug:调试信息,一般在开发中使用,是默认级别
- info:运行信息
- warn:警告信息
- error:错误信息,不会影响系统运行
- fatal:严重错误,一般会造成系统奔溃并停止运行
入门案例
第一步:引入maven依赖
xml
<!--log4j日志框架-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
第二步:编写配置文件 log4j.properties , log4j默认会去资源路径下加载这个配置文件。
properties
# 配置根记录器
log4j.rootLogger = DEBUG, console
# 配置根记录器的追加器和布局器
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %-5p %d{yyyy-MM-dd HH:mm:ss.SSS} %t %l %m%n
第三步:编写测试代码
java
import org.apache.log4j.Logger;
public class Log4jDemo1 {
private static final Logger log = Logger.getLogger(Log4jDemo1.class);
public static void main(String[] args) {
log.info("hello log4j");
log.debug("hello log4j");
}
}
第四步:运行main方法并查看结果,在控制台查看打印的日志信息
text
INFO 2026-08-05 14:38:53.027 main org.example.Log4jDemo1.main(Log4jDemo1.java:9) hello log4j
DEBUG 2026-08-05 14:38:53.027 main org.example.Log4jDemo1.main(Log4jDemo1.java:10) hello log4j
基本使用
配置文件的编写
log4j默认的配置文件叫做log4j.properties,log4j默认会去资源路径下加载这个配置文件。
配置文件的大概结构:
properties
# 第一部分:配置根日志记录器
# 配置根logger
log4j.rootLogger = [level, ] 追加器名1, 追加器名2, ...
# 配置根logger使用的追加器appender
log4j.appender.追加器名1 = 追加器的全限定类名
# 指定消息格式
log4j.appender.追加器名1.layout = 布局器的全限定类名
# 第二部分:配置自定义日志记录器
# 配置自定义的日志记录器
log4j.logger.自定义Logger = [level, ] 追加器名1, 追加器名2, ...
# 配置自定义日志记录器使用的追加器,和配置根日志记录器使用的追加器一致
log4j.appender.追加器名1 = 追加器的全限定类名
日志记录器的层级关系
日志记录器的层级关系:
- 层级关系用 "." 表示,如:"com.foo" 是 "com.foo.Bar" 的父Logger
- 根 Logger,是所有层级结构的顶部Logger,可通过名称检索获取
- 每个Logger都会被指定一个日志优先级,如果没有指定,默认继承父Logger的优先级,并且Logger只会记录等级程度大于等于自己的日志,日志的严重程度越高,等级越高。
常见的追加器
把日志打印到控制台的追加器:ConsoleAppender,它的常见选项
- Threshold = 消息级别:指定日志消息输出的级别
- ImmediateFlush = true:默认true,所有消息都会被立即输出
- Target=System.err:默认值,表示错误输出
把日志打印到文件中的追加器:FileAppender
- File = 路径:表示日志文件的路径
- Append = true:表示把日志信息追加到指定文件中
按时间创建新日志文件的追加器:DailyRollingFileAppender
- DatePattern="yyyy-MM-dd-HH-mm":可以设定每天滚动一次,或者每小时滚动一次
按大小创建新日志文件的追加器:RollingFileAppender
- MaxFileSize=100KB,文件到达该大小时,将会自动滚动,即将原来的内容移到 .log.1 文件
常见的布局器
PatternLayout
它是使用最多的布局器,它使用指定的格式符来描述日志的格式
常用格式符:
- %m:输出代码中指定的日志信息
- %p:输出优先级
- %n:换行符
- %r:输出自应用启动到输出该log信息耗费的毫秒数
- %c:输出打印语句所属类的全名
- %t:输出产生该日志的线程名
- %d:输出服务器的当前时间 %d{yyyy-MM-dd HH:mm:ss.SSS}
- %l:输出日志发生的位置,包括类的全限定名和日志语句所处的行号
- %L:输出代码中的行号
- %M:输出方法名
- %%:输出一个百分号
指定输出字符的长度:
- %5c:输出字符内容,最小宽度是5,宽度小于5,默认向右对齐
- %-5c:输出字符内容,最小宽度是5,宽度小于5,默认向左对齐
- %.5c、%20.30c:如果比较长就会截掉多余的字符
实战案例
配置自定义的记录器
使用自定义记录器,把不同类型的日志输出到不同的文件中,比如把用户行为输入到一个文件中,把系统信息输入到一个文件中。
第一步:编写配置文件
properties
# 配置根记录器
log4j.rootLogger = debug, file
# 配置根记录器的追加器和布局器
log4j.appender.file = org.apache.log4j.FileAppender
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = %p %d{yyyy年MM月dd日 HH:mm:ss.SSS} %c{5} %m%n
log4j.appender.file.file = logs/log4j.log
log4j.appender.file.encoding = UTF-8
# 配置自定义的日志记录器
log4j.logger.userLog = info, user_log
log4j.logger.sysLog = debug, sys_log
# 配置useLog的追加器和布局器
log4j.appender.user_log = org.apache.log4j.RollingFileAppender
log4j.appender.user_log.layout = org.apache.log4j.PatternLayout
log4j.appender.user_log.layout.ConversionPattern = %p %d{yyyy年MM月dd日 HH:mm:ss.SSS} %c{5} %m%n
log4j.appender.user_log.file = logs/user_log.log
log4j.appender.user_log.encoding = UTF-8
log4j.appender.user_log.MaxFileSize = 10240KB
# 配置sysLog的追加器和布局器
log4j.appender.sys_log = org.apache.log4j.RollingFileAppender
log4j.appender.sys_log.layout = org.apache.log4j.PatternLayout
log4j.appender.sys_log.layout.ConversionPattern = %p %d{yyyy年MM月dd日 HH:mm:ss.SSS} %c{5} %m%n
log4j.appender.sys_log.file = logs/sys_log.log
log4j.appender.sys_log.encoding = UTF-8
log4j.appender.sys_log.MaxFileSize = 10240KB
第二步:编写代码
java
import org.apache.log4j.Logger;
public class Log4jDemo2 {
// 自定义记录器,记录用户信息
private static final Logger useLog = Logger.getLogger("userLog");
// 自定义记录器,记录系统信息
private static final Logger sysLog = Logger.getLogger("sysLog");
// 这里默认获取的是根记录器
private static final Logger logger = Logger.getLogger(Log4jDemo2.class);
public static void main(String[] args) {
useLog.info("用户信息");
sysLog.info("系统信息");
sysLog.debug("系统debug信息");
logger.info("根记录器");
}
}
第三步:查看结果,根记录加载器对应的文件中会记录所有信息,每个自定义记录器中记录各自的信息。
这里展示根记器对应中的文件中的内容:
text
INFO 2026-08-10 19:56:13.501 org.example.Log4jDemo2 用户信息
INFO 2026-08-10 19:56:13.501 org.example.Log4jDemo2 系统信息
DEBUG 2026-08-10 19:56:13.501 org.example.Log4jDemo2 系统debug信息
INFO 2026-08-10 19:56:13.501 org.example.Log4jDemo2 根记录器
总结: 配置自定义记录器的步骤
- 第一步:在配置文件中配置自定义记录器
properties
log4j.logger.记录器名 = 日志级别, 追加器名, ...
- 第二步:在类中,通过代码
Logger logger = Logger.getLogger("记录器名");来获取自定义的记录器。 实例化一个logger,如果这个名字在配置文件中定义了,则这个logger有其相关定义了的属性,如果没有,则看是否定义了其父类的时候设置了继承。如果都没有,则拥有根记录器的属性
logback
简介
一款开源日志框架,用于Java项目,它出现的目的是为了改善log4j,而且它也是由log4j的作者编写的,是springboot默认使用的日志框架
logback分为3个模块:
- logback-core:通用模块,是其它模块的基础
- logback-classic:日志模块,完整实现了 SLF4J API
- logback-access:配合Servlet容器,提供 http 访问日志功能
logback中的核心组件:
- 日志记录器:Logger,由用户调用,负责记录日志
- 追加器:Appender,由日志记录器调用,将日志打印到目的地
- 日志输出格式:Layout,控制日志的输出格式
- 编码器:Encoder,Layout的替代品,它可以控制字节的格式
入门案例
第一步:maven依赖,logback-classic背后会自动引入 logback-core、slf4j 这两个依赖包
xml
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
第二步:在代码中使用logback
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogbackDemo1 {
// 使用slf4j的api来获取Logger实例,logback原生实现了slf4j
private static final Logger log = LoggerFactory.getLogger(LogbackDemo1.class);
public static void main(String[] args) {
// 打印日志
log.debug("hello logback");
}
}
第三步:查看结果,在控制台上输出下列日志
text
15:10:21.470 [main] DEBUG org.example.LogbackDemo1 - hello logback
基本使用
配置文件
logback默认的配置文件是 logback.xml ,如果用户没有提供,它会使用默认配置
配置文件的基本结构和内容讲解:
xml
<!--
configuration:根节点,它有三个属性:
scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!--定义变量,定义后,可以使${变量名}来使用变量-->
<property name="AppName" value="demo"/>
<!--logger上下文:每个logger都关联到logger上下文,默认上下文名称为"default"。
但可以使用contextName标签设置成其他名字,用于区分不同应用程序的记录-->
<contextName>${AppName}</contextName>
<!-- 追加器:负责写日志的组件 -->
<appender>
<!--负责指定日志的格式-->
<encoder></encoder>
</appender>
<!---根日志记录器,有且只有一个level属性,用于指定日志级别-->
<root level="INFO">
<appernder-ref ref="appenderName"/>
</root>
<!--自定义日志记录器:用来设置某一个包或者具体的某一个类的日志打印级别以及指定appender。-->
<logger>
<appender-ref ref="appenderName"/>
</logger>
</configuration>
logback的默认配置:这是模拟的
xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
logback中的组件
logback中的基本组件,和log4j中的类似,也是记录器、追加器、布局器等,它还额外提供了过滤器、编码器等
过滤器
过滤器:可以过滤日志内容,让用户可以只关注自己关心的日志。过滤器是定义在追加器中的
案例1:级别过滤器,只打印DEBUG级别的日志
xml
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
案例2:阈值过滤器,打印INFO级别以上的日志
xml
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
控制日志格式的格式替身符
关于日志调用位置的格式描述符:
- %logger{长度}、%c、%lo:日志所在类的全名,默认可以不指定长度,如果指定后,超出长度,包名会被缩略,但是类名不会
- %class、%C:输出调用日志记录器的类的全限定名,尽量避免使用
- %F、%file:输出调用日志记录器的java源文件名,尽量避免使用
- %caller{depth}:输出调用日志记录器的位置信息,整数选项表示输出信息深度
- %L、%line:输出执行日志请求的行号,尽量避免使用
- %M、%method:输出执行日志请求的方法名,尽量避免使用
关于时间的格式描述符:
- %d{pattern}、%date{pattern}:输出时间,配置案例:%d{yyyy-MM-dd HH:mm:ss.SSS}
- %r、%relative:输出从程序启动到创建日志记录的时间,单位是毫秒
关于日志信息的格式描述符:
- %m、%msg、%message:输出日志信息
- %n:输出换行符
- %%:输出一个百分号
关于日志级别的格式描述符:
- %p、%le、%level:输出日志级别
关于线程的日志描述符:
- %t、%thread:输出产生日志的线程名
日志文件:
- %i:用于日志文件的名称中,表示序号
其它:
- %file:文件名,等价于%F
- %line:行号,等价于%L
- %relative:输出自应用启动到输出该日志讯息所耗费的毫秒数,等价于%r
- %thread:输出产生该日志事件的线程名,等价于 %t
案例:
text
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56}.%method:%L - %msg%n
打印结果:
text
2024-03-23 11:09:59.863 [main] INFO org.example.LogBackTest.main:12 - hello world
彩色日志
案例:
text
%green(%date{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %red([%thread]) %boldMagenta(%logger{50}.%method:%L) %cyan(%msg%n)
实战案例
编写配置文件,要求实现的功能:
- 把日志分别输出到控制台和文件中,
- 控制台输出info级别的彩色日志,
- 把全部的日志输出的一个文件中
- 把指定目录下的info级别的日志输出到一个文件中,方便查看运行情况
- 文件按天滚动
- 自定义日志记录器,把指定包下的日志输出到指定文件中
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!--禁止logback的内部日志-->
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<property name="LOG_CONTEXT_NAME" value="logback-test"/>
<!--定义日志文件的存储地址-->
<property name="LOG_HOME" value="logs/${LOG_CONTEXT_NAME}"/>
<!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,
%method 方法名 ,%L 行数,%msg:日志消息,%n是换行符-->
<property name="pattern"
value="%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %logger{56}.%method:%L - %msg%n" />
<property name="dayInfoFile"
value="${LOG_HOME}/%d{yyyy-MM-dd}/info.%d{yyyy-MM-dd}.%i.log" />
<property name="hourInfoFile"
value="${LOG_HOME}/%d{yyyy-MM-dd}/info.%d{yyyy-MM-dd-HH}.%i.log" />
<property name="myInfoFile"
value="${LOG_HOME}/%d{yyyy-MM-dd}/my-info.%d{yyyy-MM-dd}.%i.log" />
<!-- 定义日志上下文的名称 -->
<contextName>${LOG_CONTEXT_NAME}</contextName>
<!-- 控制台输出,输出info级别的日志 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%green(%date{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %red([%thread]) %boldMagenta(%logger{50}.%method:%L) %cyan(%msg%n)</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 级别过滤器:只接受INFO及以上,拒绝DEBUG,之所以要在这里过滤日志级别,是因为
console appender会被绑定到根日志记录器上,根日志记录器的日志级别最好是debug
级别,方便查看详细日志。一定要把console appender绑定到根日志记录器上,是因为
第三方库的日志会被上传根日志记录器而不会上传到自定义日志记录器,想在控制台查看
第三方框架的日志,只能把console appender绑定到根日志记录器上 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--文件输出,输出info级别的日志-->
<appender name="file.info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名,按天生成-->
<FileNamePattern>${dayInfoFile}</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
<!-- 除按日志记录之外,还配置了日志文件不能超过10M(默认),若超过10M,日志文件会以索引0开始 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<!--文件输出,输出info级别的日志-->
<appender name="my.file.info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名,按小时生成-->
<!--<FileNamePattern>${hourInfoFile}</FileNamePattern>-->
<!--日志文件输出的文件名,按天生成-->
<FileNamePattern>${myInfoFile}</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
<!-- 除按日志记录之外,还配置了日志文件不能超过10M(默认),若超过10M,日志文件会以索引0开始 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="debug">
<appender-ref ref="console"/>
<appender-ref ref="file.info"/>
</root>
<!-- 自定义日志记录器:某类/包下的所有日志使用file.info appender输出,
additivity等于false表示日志不再向上传递-->
<logger name="org.example" additivity="true" level="info">
<appender-ref ref="my.file.info"/>
</logger>
</configuration>
第二步:Java代码
java
// 第一个类
package org.example;
import org.example.mlog.MyLoggerTest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogbackDemo2 {
private static final Logger log = LoggerFactory.getLogger(LogbackDemo2.class);
public static void main(String[] args) {
// 打印日志
log.info("hello logback");
// 调用设置了自定义Logger的类
MyLoggerTest.test();
try {
int i = 10 / 0;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
// 第二个类: 调用自定义日志记录器,注意在哪个包下
package org.example.mlog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyLoggerTest {
private static final Logger log = LoggerFactory.getLogger(MyLoggerTest.class);
public static void test() {
log.debug("调用myLoggerTest");
}
}
第三步:打印结果
文件1:info.2024-03-23.0.log
xml
INFO 2026-08-05 15:13:02.646 main org.example.LogbackDemo2.main:12 - hello logback
ERROR 2026-08-05 15:13:02.649 main org.example.LogbackDemo2.main:20 - / by zero
java.lang.ArithmeticException: / by zero
at org.example.LogbackDemo2.main(LogbackDemo2.java:18)
文件2:my-info.2024-03-23.0.log,自定义日志记录器对应的文件
text
DEBUG 2026-08-05 15:13:02.648 main org.example.mlog.MyLoggerTest.test:9 - 调用myLoggerTest
总结: 每次程序启动,如果日志文件没有达到指定阈值,会继续在原有的文件中打印日志
log4j2
简介
log4j的升级版,参考了logback的一些设计,并修复了一些问题
优点:
- 性能提升
- 自动重载配置:可以动态的修改配置而不需要重启应用
- 无垃圾机制:避免频繁的日志收集导致的jvm gc
入门案例
第一步:添加依赖
xml
<!--使用slf4j作为日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<!--使用log4j2的适配器进行绑定-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.9.1</version>
</dependency>
<!--log4j2日志门面-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<!--log4j2日志实现-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
第二步:编写配置文件,log4j2提供的默认实现只会把error级别的日志打印到控制台,所以这里给一个简单的配置,禁止log4j2输出自身的debug日志,同时把程序产生的日志,debug级别以上全部输出到控制台
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!--status="info" 日志框架本身的输出日志级别,可以修改为debug,要关闭的话改为off
monitorInterval="5" 自动加载配置文件的间隔时间,不低于5秒 -->
<Configuration status="OFF" monitorInterval="5">
<!--配置变量,通过${name}进行引用-->
<properties>
<property name="pattern">
%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %t %c{20}.%M:%L - %m%n
</property>
</properties>
<!-- 追加器 -->
<Appenders>
<!-- 输出到控制台的追加器-->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${pattern}" />
</Console>
</Appenders>
<!-- logger 定义 -->
<Loggers>
<!-- 定义根logger -->
<Root level="DEBUG" includeLocation="true">
<!-- 指定rootLogger使用的追加器 -->
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
第三步:Java代码
java
package org.wyj;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Log4j2Test2 {
// slf4j的api
private static final Logger logger = LoggerFactory.getLogger(Log4j2Test2.class);
public static void main(String[] args) {
logger.debug("slf4j debug {}", "aa");
logger.warn("slf4j warn");
logger.info("slf4j info");
logger.error("slf4j error");
}
}
查看打印结果:
text
DEBUG 2026-08-10 20:30:27.652 main org.example.Log4j2Demo1.main:10 - slf4j debug aa
WARN 2026-08-10 20:30:27.654 main org.example.Log4j2Demo1.main:11 - slf4j warn
INFO 2026-08-10 20:30:27.654 main org.example.Log4j2Demo1.main:12 - slf4j info
ERROR 2026-08-10 20:30:27.654 main org.example.Log4j2Demo1.main:13 - slf4j error
log4j2中的组件
日志记录器、追加器
这些基本和logback中的使用方式一样,这里不多介绍了,主要介绍一些log4j2中独有的配置
异步日志
log4j2的底层引入了disruptor,支持异步打印日志,大大提升了日志打印的性能。
在配置文件中配置异步日志: 异步打印日志使用 AsyncRoot 、AsyncLogger 标签,同步打印日志使用Root、Logger标签
xml
<loggers>
<!--定义根日志记录器-->
<AsyncRoot level="DEBUG">
<appender-ref ref="console" />
</AsyncRoot>
<!--打印业务相关的日志-->
<AsyncLogger name="org.example.blog.aop.log" level="DEBUG" additivity="true">
<appender-ref ref="BUSINESS_APPENDER" />
</AsyncLogger>
</loggers>
PatternLayout标签中的格式描述符
描述时间的:
- %d{日期格式}:显示日志记录时间,配置日期格式的案例:%d{yyyy-MM-dd HH:mm:ss.SSS}、
- %r:显示从程序启动时到记录该条日志时已经经过的毫秒数
描述线程的:
- %t:输出产生该日志事件的线程名
描述日志输出位置的:
- %l:输出日志事件的发生位置,包括类名、方法名,以及在代码中的行数,打印出的日志可以直接链接到该行
- %L:显示调用logger的代码行
- %M:显示调用logger的方法名
- %c{层数}:列出logger名字空间的全称,如果加上{层数}表示列出从最内层算起的指定层数的名字空间
- %C:列出调用logger的类的全名(包含包路径)
描述日志级别的:
- %p:显示该条日志的优先级
- %level:日志级别
描述日志内容的:
- %m:显示输出消息
- %n:当前平台下的换行符
- %%:显示一个百分号
特殊功能:
- %X:按MDC(Mapped Diagnostic Context,线程映射表)输出日志。通常用于多个客户端连接同一台服务器,方便服务器区分是那个客户端访问留下来的日志。
彩色日志的案例:
text
%green{[%date{yyyy-MM-dd HH:mm:ss}]} %highlight{%-5level} %cyan{[%thread]} %magenta{%logger} : %black{%msg%n}
同样,尽量把彩色日志输出到控制台,输出到文件中颜色字符会影响查看日志
过滤器
配置 在appender中,过滤日志,可以根据日志级别过滤日志,常用的过滤器有ThresholdFilter
ThresholdFilter,它的三个重要参数:
-
level: 标识需要过滤的日志级别,取值同上 日志级别
- INFO: info 日志级别
- DEBUG: debug 日志级别
-
onMatch:
- ACCEPT: 表示匹配该级别及以上
- DENY:表示不匹配该级别及以上
- NEUTRAL:表示该级别及以上的,由下一个filter处理;如果当前是最后一个,则同 ACCEPT
-
onMismatch:
- ACCEPT:表示匹配该级别以下
- DENY:表示不匹配该级别以下的
- NEUTRAL:表示该级别及以下的,由下一个filter处理,如果当前是最后一个,则同 DENY
案例1:打印INFO及以上级别的日志
xml
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
案例2:只打印debug级别的日志
xml
<ThresholdFilter level="INFO" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
实战案例
配置要求:
- 日志分别输出到日志文件、控制台,控制台使用彩色日志
- 日志分别按照debug级别、info级别、error级别,输出到三个日志文件中
- 每个日志文件的拆分规则: 程序启动时使用新的日志文件、文件超过10M拆分、文件在时间上按天拆分
- 指定组件产生的日志,打印到特定的文件中,方便查看,这是通过自定义日志记录器实现的,这里, org.example.blog 包下的日志打印的business.log文件中,org.example.mapper包下的日志打印到sql.log文件中,并且不向上传递,只打印到这个文件中,避免sql日志影响业务日志的查看。
- 配置使用异步日志
xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<!--变量配置-->
<Properties>
<!-- 定义日志存储的路径-->
<property name="LOG_CONTEXT_NAME">blog-api</property>
<!--定义日志文件的存储地址-->
<property name="LOG_HOME">logs/${LOG_CONTEXT_NAME}</property>
<property name="DAY_INFO_FILE">${LOG_HOME}/$${date:yyyy-MM-dd}/info.%d{yyyy-MM-dd}.%i.log</property>
<property name="DAY_BUSINESS_INFO_FILE">${LOG_HOME}/$${date:yyyy-MM-dd}/business.%d{yyyy-MM-dd}.%i.log</property>
<property name="DAY_SQL_INFO_FILE">${LOG_HOME}/$${date:yyyy-MM-dd}/sql.%d{yyyy-MM-dd}.%i.log</property>
<property name="DAY_DEBUG_FILE">${LOG_HOME}/$${date:yyyy-MM-dd}/debug.%d{yyyy-MM-dd}.%i.log</property>
<property name="DAY_ERROR_FILE">${LOG_HOME}/$${date:yyyy-MM-dd}/error.%d{yyyy-MM-dd}.%i.log</property>
<!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,
%method 方法名 ,%L 行数,%msg:日志消息,%n是换行符-->
<property name="LOG_PATTERN">
[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%thread] %logger.%method:%L : %msg%n
</property>
<!--彩色日志-->
<property name="COLOR_LOG_PATTERN">
%green{[%date{yyyy-MM-dd HH:mm:ss}]} %highlight{%-5level} %cyan{[%thread]} %magenta{%logger} : %yellow{%msg%n}
</property>
</Properties>
<appenders>
<!-- 输出到控制台的追加器-->
<Console name="console" target="SYSTEM_OUT">
<!-- 过滤器,表示当前appender只打印info及以上级别的信息 -->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${COLOR_LOG_PATTERN}" /> <!--控制台使用彩色日志-->
</Console>
<!--存储info信息-->
<RollingFile name="rollingFileInfo" fileName="${LOG_HOME}/info.log"
filePattern="${DAY_INFO_FILE}">
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!-- 在系统启动时,触发拆分规则,生产一个新的日志文件 -->
<OnStartupTriggeringPolicy />
<!--按照时间节点拆分,规则根据filePattern定义的-->
<TimeBasedTriggeringPolicy />
<!-- 按照文件大小拆分,10MB -->
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="50"/>
</RollingFile>
<!--存储debug信息-->
<RollingFile name="rollingFileDebug" fileName="${LOG_HOME}/debug.log"
filePattern="${DAY_DEBUG_FILE}">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<OnStartupTriggeringPolicy />
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="30MB"/>
</Policies>
<DefaultRolloverStrategy max="50"/>
</RollingFile>
<!--存储error信息-->
<RollingFile name="rollingFileError" fileName="${LOG_HOME}/error.log"
filePattern="${DAY_ERROR_FILE}">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<OnStartupTriggeringPolicy />
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="50"/>
</RollingFile>
<!-- 记录业务相关的信息 -->
<RollingFile name="BUSINESS_APPENDER" fileName="${LOG_HOME}/business.log"
filePattern="${DAY_BUSINESS_INFO_FILE}">
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${LOG_PATTERN}" charset="utf-8"/>
<Policies>
<OnStartupTriggeringPolicy />
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="50"/>
</RollingFile>
<!-- 记录sql相关的信息 -->
<RollingFile name="SQL_APPENDER" fileName="${LOG_HOME}/sql.log"
filePattern="${DAY_SQL_INFO_FILE}">
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="${LOG_PATTERN}" charset="utf-8"/>
<Policies>
<OnStartupTriggeringPolicy />
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="50"/>
</RollingFile>
</appenders>
<loggers>
<!--定义根日志记录器-->
<AsyncRoot level="DEBUG">
<appender-ref ref="console" />
<appender-ref ref="rollingFileInfo"/>
<appender-ref ref="rollingFileDebug"/>
<appender-ref ref="rollingFileError"/>
</AsyncRoot>
<!--打印业务相关的日志-->
<AsyncLogger name="org.example.blog" level="DEBUG" additivity="true">
<appender-ref ref="BUSINESS_APPENDER" />
</AsyncLogger>
<!--打印sql相关的日志-->
<AsyncLogger name="org.example.mapper" level="DEBUG" additivity="false">
<appender-ref ref="SQL_APPENDER" />
</AsyncLogger>
</loggers>
</Configuration>
1、RollingFile标签定义的拆分规则
- 项目启动后,最先生成的日志文件是使用filename属性定义的文件,
- 如果达到拆分标准,则依据filePattern的值,生成新的文件,将filename文件中的数据迁移到filePattern文件中,
- 然后向filename文件中写入最新的数据。
- 每次项目启动时,filename文件中的数据会被写入到最新的filePattern日志文件中,然后向filename文件中写入日志信息
jul
jul:java common logging,java自带的日志框架。jul的配置文件名称是 logging.properties
入门案例
java
import java.util.logging.Logger;
public class JulDemo1 {
private static final Logger log = Logger.getLogger(JulDemo1.class.getName());
public static void main(String[] args) {
log.info("hello jul");
}
}
在控制台打印出的日志:
text
八月 11, 2026 5:16:14 下午 org.example.JulDemo1 main
信息: hello jul
jcl
jcl:Jakarta Commons Logging,日志门面框架,是apache提供的。它默认的配置文件是 commons-logging.properties
入门案例
第一步:添加依赖
xml
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
第二步:测试
java
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class JclDemo1 {
// jcl的两个基本抽象:Log,基本记录器,LogFactory,负责创建Log实例
private static final Log log = LogFactory.getLog(JclDemo1.class);
public static void main(String[] args) {
log.info("hello jcl");
}
}
打印出的日志:
text
八月 11, 2026 5:19:51 下午 org.example.JclDemo1 main
信息: hello jcl
看起来jcl默认使用的使用jul提供的日志实现
总结
尽管日志框架有多个,但它们都包含一些共性的概念:
- 日志记录器:Logger,用户通过日志记录器来记录日志
- 日志追加器:Appender,日志记录器通过调用日志追加器来实现记录日志的功能
- 格式工具Layout和格式描述符:由日志追加器调用,渲染日志的格式
- 日志级别:不同的框架有不同的定义,但大概都有4个:debug、warn、info、error,记录的信息越全面,则日志的级别越低,低级别的日志中默认包含高级别的日志中的内容,例如,debug级别的日志中有info级别的日志的信息
- 日志文件的合并策略:在长期运行的大型项目中,日志信息必然很多,这些记录了日志信息的文件该如何生成又如何合并?所以需要配置文件的生成和合并策略
不同的日志框架,还提供了一些自有的功能,例如,动态修改日志级别、异步打印日志等。
源码解析
slf4j
slf4j是怎么找到日志实现框架的?
下面就是核心逻辑: LoggerFactory.getLogger,获取日志记录器的时候,会执行加载的逻辑,并且只会执行一次
java
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
这段代码的含义是,加载指定的类 'org/slf4j/impl/StaticLoggerBinder.class' 到内存中,日志实现框架只需要提供一个这个类,slf4j在这里会把这个类加载到内存中,然后调用这个类,来获取日志记录器工厂,再从工厂中获取日志记录器。这里类似于SPI机制,第三方日志实现框架,按照slf4j的要求,提供实现类,从而完成桥接.
如果类路径下有多个StaticLoggerBinder怎么办?slf4j默认取出现的第一个。
log4j
log4j是如何解析它的配置文件log4j.properties的?
基本的流程就是加载properties文件,按照预先设定好的前缀,解析文件中的每一行。
这里重点介绍一下,log4j是如何解析日志格式描述符的,例如 %-5p %d{yyyy-MM-dd HH:mm:ss.SSS} %l %m%n,类似这样的描述符是如何被解析的。 大致流程中,遍历这个字符串中的每一个字符,最初的时候,匹配普通字符,如果是一个普通字符,创建一个普通字符转换器,直接把普通字符打印到日志中,例如,案例中的空格,每个空格都会创建一个对应的转换器实例,负责打印空格,如果匹配到百分号,就转换模式,下一次字符作为转义符来进行匹配,后面如果是中横杠,表示左对齐,如果是数字,表示最长长度,如果是p、d这样子的,就创建一个专门的转换器,每种类型的格式描述符都会有一个专门的转换器来处理它的逻辑。 最终,日志格式描述符中的每个转义符,如 %p、%d,每个普通字符,如空格等,都会创建一个专门的转换器,转换器之间组成一个单向链表,格式化器持有这个单向链表的头节点,每个节点处理完之后,调用下一个节点,直到所有节点都处理完成。
核心代码:
java
// 构造解析器,这里传入的就是 %-5p %d{yyyy-MM-dd HH:mm:ss.SSS} %l %m%n 这样的格式描述符
public PatternParser(String pattern) {
this.pattern = pattern;
patternLength = pattern.length();
state = LITERAL_STATE;
}
// 解析格式描述符
public PatternConverter parse() {
char c;
i = 0;
// 遍历格式描述符中的每一个字符
while (i < patternLength) {
c = pattern.charAt(i++);
switch (state) {
// 1、 匹配普通字符
case LITERAL_STATE: // private static final int LITERAL_STATE = 0;
// In literal state, the last char is always a literal.
if (i == patternLength) {
currentLiteral.append(c);
continue;
}
// 如果是 % ,并且百分号后面不是 %或n,触发模式切换,下一个字符匹配转义符,
if (c == ESCAPE_CHAR) {
// peek at the next char.
switch (pattern.charAt(i)) {
case ESCAPE_CHAR:
currentLiteral.append(c);
i++; // move pointer
break;
case 'n':
currentLiteral.append(Layout.LINE_SEP);
i++; // move pointer
break;
default:
if (currentLiteral.length() != 0) {
addToList(new LiteralPatternConverter(
currentLiteral.toString()));
// LogLog.debug("Parsed LITERAL converter: \""
// +currentLiteral+"\".");
}
currentLiteral.setLength(0);
currentLiteral.append(c); // append %
state = CONVERTER_STATE;
formattingInfo.reset();
}
} else {
currentLiteral.append(c);
}
break;
// 2、 匹配转义符,如果是 - 、 . 等,则触发模式切换,匹配格式描述符
case CONVERTER_STATE:
currentLiteral.append(c);
switch (c) {
case '-':
formattingInfo.leftAlign = true;
break;
case '.':
state = DOT_STATE;
break;
default:
if (c >= '0' && c <= '9') {
formattingInfo.min = c - '0';
state = MIN_STATE;
} else
// 最常见的场景,匹配 %p、%d 中的p、d等
finalizeConverter(c);
} // switch
break;
// 如果上一个字符是 - ,触发当前模式
case MIN_STATE:
currentLiteral.append(c);
if (c >= '0' && c <= '9')
formattingInfo.min = formattingInfo.min * 10 + (c - '0');
else if (c == '.')
state = DOT_STATE;
else {
finalizeConverter(c);
}
break;
case DOT_STATE:
currentLiteral.append(c);
if (c >= '0' && c <= '9') {
formattingInfo.max = c - '0';
state = MAX_STATE;
} else {
LogLog.error("Error occured in position " + i
+ ".\n Was expecting digit, instead got char \"" + c + "\".");
state = LITERAL_STATE;
}
break;
case MAX_STATE:
currentLiteral.append(c);
if (c >= '0' && c <= '9')
formattingInfo.max = formattingInfo.max * 10 + (c - '0');
else {
finalizeConverter(c);
state = LITERAL_STATE;
}
break;
} // switch
} // while
if (currentLiteral.length() != 0) {
addToList(new LiteralPatternConverter(currentLiteral.toString()));
// LogLog.debug("Parsed LITERAL converter: \""+currentLiteral+"\".");
}
return head;
}
转换器的实例:
java
// 时间转换器,打印日志中的时间,底层使用的使用 SimpleDateFormat
private static class DatePatternConverter extends PatternConverter {
private DateFormat df;
private Date date;
DatePatternConverter(FormattingInfo formattingInfo, DateFormat df) {
super(formattingInfo);
date = new Date();
this.df = df;
}
// 把日志发生时间转换为指定格式
public String convert(LoggingEvent event) {
date.setTime(event.timeStamp);
String converted = null;
try {
converted = df.format(date);
} catch (Exception ex) {
LogLog.error("Error occured while converting date.", ex);
}
return converted;
}
}
log4j是如何创建日志记录器的?
第一步: 程序启动时加载配置文件。 如果用户在自己的代码中编写了 private static final Logger log = Logger.getLogger(UserService.class); 这样的代码,在jvm加载UserService时,会执行初始化的逻辑,会触发加载Logger类,Logger类中的getLogger方法又会触发加载LogManger,在LogManage的静态代码块中,会执行log4j的初始化逻辑,
java
public class LogManager {
/**
* @deprecated This variable is for internal use only. It will
* become package protected in future versions.
* */
static public final String DEFAULT_CONFIGURATION_FILE = "log4j.properties";
static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml";
// 初始化的逻辑
static {
// 创建根日志记录器,Hierarchy是最核心的类,负责创建、缓存日志记录器,维护日志记录器的层级关系
Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
repositorySelector = new DefaultRepositorySelector(h);
/** Search for the properties file log4j.properties in the CLASSPATH. */
String override =OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY,
null);
// if there is no default init override, then get the resource
// specified by the user or the default config file.
if(override == null || "false".equalsIgnoreCase(override)) {
// 解析配置文件
String configurationOptionStr = OptionConverter.getSystemProperty(
DEFAULT_CONFIGURATION_KEY,
null);
String configuratorClassName = OptionConverter.getSystemProperty(
CONFIGURATOR_CLASS_KEY,
null);
URL url = null;
// ... 省略代码
}
}
创建日志记录器的核心逻辑:
java
// 参数中,name是用户传入的类名,例如 org.example.UserService,
// factory是日志记录器工厂实例
public Logger getLogger(String name, LoggerFactory factory) {
CategoryKey key = new CategoryKey(name);
Logger logger;
synchronized (ht) {
Object o = ht.get(key);
if (o == null) {
// 创建日志记录器
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateParents(logger);
return logger;
} else if (o instanceof Logger) {
return (Logger) o;
} else if (o instanceof ProvisionNode) {
// System.out.println("("+name+") ht.get(this) returned ProvisionNode");
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateChildren((ProvisionNode) o, logger);
updateParents(logger);
return logger;
} else {
// It should be impossible to arrive here
return null; // but let's keep the compiler happy.
}
}
}
大致逻辑是: 根据key,从hashTable中获取日志记录器,如果获取不到,就创建一个,这里就是new Logger,同时传入一个name做参数,然后为日志记录器设置父记录器,寻找父记录器的逻辑是,把类的全限定名按照 "." 分隔符切分,例如,在这里,会寻找有没有名为 "org.example" 的日志记录器,如果没有,寻找有没有名为 "org" 的日志记录器,如果都没有,把根日志记录器设置为当前记录器的父记录器。
log4j是如何打印日志的?
基本流程: 日志记录器持有追加器的实例,追加器又持有格式化器的实例。 打印日志时,日志记录器调用追加器,如果没有,则调用父类的,追加器先调用格式化器,把日志拼接上日志级别、代码位置等信息,然后追加器再把日志打印到指定位置,如果是文件,则是一个输出流,如果是jdbc等,则是数据库连接。
log4j是如何获取日志所在行号的?
通过反射,拿到当前的堆栈信息,从栈顶,也就是main方法所在位置,逐步线下遍历,直到找到Logger类,Logger类的上一个类,就是调用日志记录器的位置信息。
log4j获取类名、方法名、行号等信息的方法: 位于StackTraceElement类中
java
try {
Class[] noArgs = null;
// 获取堆栈信息的方法
getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs);
Class stackTraceElementClass = Class.forName("java.lang.StackTraceElement");
// 获取类名
getClassNameMethod = stackTraceElementClass.getMethod("getClassName", noArgs);
// 获取方法名
getMethodNameMethod = stackTraceElementClass.getMethod("getMethodName", noArgs);
// 获取文件名
getFileNameMethod = stackTraceElementClass.getMethod("getFileName", noArgs);
// 获取行号
getLineNumberMethod = stackTraceElementClass.getMethod("getLineNumber", noArgs);
} catch(ClassNotFoundException ex) {
LogLog.debug("LocationInfo will use pre-JDK 1.4 methods to determine location.");
} catch(NoSuchMethodException ex) {
LogLog.debug("LocationInfo will use pre-JDK 1.4 methods to determine location.");
}
总结: 获取位置信息的代价比较大,建议不要是%M、%l等输出位置信息,只使用%c输出日志记录器名称即可,打印日志时的消耗会大大降低。
logback
logback是怎么做到原生实现slf4j-api的?
在logback-classic中,实现了slf4j-api中的接口,并且提供 org.slf4j.impl.StaticLoggerBinder 类,之前解析slf4j的源码时提到过,slf4j内部会加载当前类路径下的StaticLoggerBinder类,这个类不可以有多个,否则会报错,这里,这个类是logback提供的,会加载logback相关的组件,所以用户面向slf4j编程,但是实际调用的是logback的实例。
logback的基本架构和log4j1.x基本相同,不过它优化了锁粒度,减少或消除锁竞争,重写了核心代码,性能相较于log4j1.x有了很大的提升。
log4j2
log4j2底层是怎么使用disruptor异步打印日志的?
log4j2引入了disruptor,无锁队列,相较于logback使用的阻塞队列,多线程并发写入时,线程之间会产生激烈的锁竞争,当业务流量很大时,业务线程会因为锁竞争而频繁阻塞,整体吞吐量不升反降,log4j2直接使用了无锁队列,业务线程把日志放入到队列中时不需要锁竞争,性能大大提升。
此外,log4j2采用了更细粒度的锁机制,整体追求不可变和无锁,log4j2的大部分组件被设计成不可变的,消除共享资源,避免使用锁。
日志框架实战
springboot整合logback
springboot默认的日志框架就是logback,几乎不用手动整合,不过,这里介绍一下它的内部原理:springboot的启动器 spring-boot-starter 中,依赖 spring-boot-starter-logging,
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.1.5.RELEASE</version>
<scope>compile</scope>
</dependency>
spring-boot-starter-logging的内部,依赖logback
xml
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.26</version>
<scope>compile</scope>
</dependency>
</dependencies>
这里的三个依赖: logback-classic,logback本身,log4j-to-slf4j,log4j到slf4j的适配器,jul-to-slf4j,jul到slf4j的适配器,
除了jul本身的依赖,另外两个适配器是做什么的? 已 jul-to-slf4j 为例,如果当前项目依赖的第三方库,内部使用jul打印日志,要想把第三方库的日志管理起来,输出到指定的文件,方便查看,就需要使用到这个适配器,springboot在启动时,会执行这个适配器,在它的内部,如果检测到jul的包,就会获取它的根日志记录器,移除里面的hander,加入自己的handler,这个handler会把日志请求转发给slf4j的实现类。 这就是这个适配器的工作原理,另一个也是同样的作用。
springboot整合log4j2
整合方式:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
<!--log4j2异步打印日志需要依赖disruptor-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.7</version>
</dependency>
排除 starter-logging ,引入 starter-log4j2 即可。
starter-log4j2中有什么? log4j对于slf4j的适配器、log4j本身的依赖,还有log4j到jul的转换器。
第三方库内部是怎么打印日志的?
以mybatis为例。 mybatis内部实现了一套自己的日志抽象,org.apache.ibatis.logging.Log,在 org.apache.ibatis.logging.LogFactory 中,会自动探测当前类路径下有哪些日志框架,然后尝试使用这些日志框架来作为日志实现。
加载方式:
java
public final class LogFactory {
/**
* Marker to be used by logging implementations that support markers
*/
public static final String MARKER = "MYBATIS";
private static Constructor<? extends Log> logConstructor;
// 在静态代码块中尝试加载日志实现
static {
tryImplementation(new Runnable() {
@Override
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useCommonsLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useLog4J2Logging();
}
});
// ... 省略代码
}
}
// 以加载slf4j为例
// 1. tryImplementation,尝试加载日志框架
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
// 2. 这是上面LogFactory静态代码块中最先尝试加载的
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
// 3. Slf4jImpl的构造方法: 如果类路径下没有slf4j的api,执行这个方法会报错,然后外部吞掉异常,然后继续加载其他实现。
public Slf4jImpl(String clazz) {
Logger logger = LoggerFactory.getLogger(clazz);
if (logger instanceof LocationAwareLogger) {
try {
// check for slf4j >= 1.6 method signature
logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
return;
} catch (SecurityException e) {
// fail-back to Slf4jLoggerImpl
} catch (NoSuchMethodException e) {
// fail-back to Slf4jLoggerImpl
}
}
// Logger is not LocationAwareLogger or slf4j version < 1.6
log = new Slf4jLoggerImpl(logger);
}
假如我要开发一个独立的三方库,内部该怎么打印日志,才能方便别人使用我的库? 第三方库只依赖slf4j-api,不依赖日志实现,方便别人调用,如果要开发调试,可以在测试范围依赖一个日志实现 <scope>test</scope>。
使用traceId来追踪一个完整的请求链路
这里使用的日志框架是slf4j + log4j2。 使用最简单的方式来接入traceId,编写一个过滤器,拦截当前服务中的所有http请求,如果请求头中有traceId,使用请求头中的,如果请求头中没有,自己设置一个traceId,请求处理完之后移除traceId。
基本过程:
1、 配置日志格式,在里面加入 %X{traceId} , 这样每行日志都会加上tracId
2、 配置过滤器:
java
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "traceId";
// header名称,上游网关可以把traceId放在这个header往下传
private static final String TRACE_HEADER = "X‑Trace‑Id";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
//1.优先从header拿traceId;没有就生成新的
String traceId = request.getHeader(TRACE_HEADER);
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
try {
// 放入MDC,当前线程所有日志生效
MDC.put(TRACE_ID, traceId);
// 响应头也返回traceId,方便前端排查
response.setHeader(TRACE_HEADER, traceId);
filterChain.doFilter(request, response);
} catch (Exception e) {
logger.error("设置traceId失败", e);
} finally {
// ⚠️必须清理!!!tomcat线程池复用线程,不清理会traceId串到下一个请求
MDC.remove(TRACE_ID);
}
}
}
import org.example.filter.TraceIdFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TraceConfig {
@Bean
public FilterRegistrationBean<TraceIdFilter> traceFilter() {
FilterRegistrationBean<TraceIdFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new TraceIdFilter());
bean.addUrlPatterns("/*");
bean.setOrder(1); // 尽量靠前,优先执行
return bean;
}
}
完成。
原理讲解: slf4j会把traceId设置到ThreadLocal中,所以,如果处理请求的过程中需要切换线程,traceId需要手动传递,类似的,还有mq、定时任务等位置,如果需要追踪请求链路,traceId都需要手动传递。 然后打印日志时,会从ThreadLocal中获取traceId。