Appearance
在Spring Boot中使用日志
本文主要介绍如何在Spring Boot中使用日志。默认情况下,Spring boot使用**slf4j(日志门面,即接口)+logback(日志实现)**作为日志框架,本文默认情况下即以该套组合讲解。
1. 基本使用
只要我们的项目中引入了以下依赖(这个依赖在spring-boot-starter中有引入,所以不用我们手动引入),就可以使用日志:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>3.4.0</version>
<scope>compile</scope>
</dependency>在spring-boot-starter-logging中,依赖如下:
xml
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.24.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>2.0.17</version>
<scope>compile</scope>
</dependency>
</dependencies>可以看到,自动引入了logback作为日志实现。
关于log4j-to-slf4j和jul-to-slf4j的作用:
在大型项目中,可能会引入多个第三方库,而这些库可能各自使用了不同的日志框架,例如一些库使用 Log4j 2,另一些可能使用 Logback、Log4j 1.x 或 java.util.logging。
log4j-to-slf4j:如果没有log4j-to-slf4j,那么当一个库通过 Log4j 2 API 打印日志时,它会尝试寻找 Log4j 2 的实现来处理这些日志。但如果希望所有的日志都由 Logback 统一处理,就需要这个桥接器。当应用程序或其依赖项调用 Log4j 2 的 API(例如org.apache.logging.log4j.Logger.info(...))时,log4j-to-slf4j会截获这些调用,并将它们转发给 SLF4J 的 API。这样,SLF4J 再根据其绑定的实际日志实现(这里是 Logback)来处理这些日志事件。它确保了任何使用 Log4j 2 的模块最终都能将其日志输出到由 SLF4J 代理、Logback 实现的日志系统。jul-to-slf4j:同理,jul-to-slf4j是一个java.util.logging(JUL) 到 SLF4J 的适配器或桥接器。它的作用是将使用 Java 标准库的java.util.loggingAPI 记录的日志事件重定向到 SLF4J API。
开箱即用:
java
@Test
void test01(){
// 1. 通过slf4j提供的日志工厂,获取日志记录器
Logger logger = LoggerFactory.getLogger(LogTest.class);
// 2. 使用日志记录器写日志
logger.info("日志测试...");
}在lombok中,为我们提供了注解@Slf4j,使得我们不用手动获取日志记录器,可以直接使用变量log记录日志:
java
@Slf4j
@SpringBootTest
public class LogTest {
@Test
void test01(){
Logger logger = LoggerFactory.getLogger(LogTest.class);
logger.info("日志测试...");
}
@Test
void test02(){
log.warn("@Slf4j注解测试写日志...");
}
}2. 日志配置
在application.yml文件中,我们可以对日志进行配置:
2.1 日志记录级别
我们可以通过以下配置设置日志记录级别:
yaml
logging:
level:
root: info可以设置的级别如下:
txt
TRACE
DEBUG
INFO(默认级别)
WARN
ERROR
FATAL(与ERROR相同)
OFF(不输出日志)优先级从低到高,比如,默认级别为info,那么当使用log.debug()记录日志时,就不会打印。
java
@Test
void testLevel(){
log.trace("trace日志...");
log.debug("debug日志...");
log.info("info日志...");
log.warn("warn日志...");
log.error("error日志...");
}结果如下:
txt
2024-12-22T16:18:16.555+08:00 INFO 69142 --- [springboot-demo] [ main] org.example.springbootdemo.LogTest : info日志...
2024-12-22T16:18:16.555+08:00 WARN 69142 --- [springboot-demo] [ main] org.example.springbootdemo.LogTest : warn日志...
2024-12-22T16:18:16.555+08:00 ERROR 69142 --- [springboot-demo] [ main] org.example.springbootdemo.LogTest : error日志...可以看到trace和debug日志没有输出。
2.2 分组记录
所谓分组,就是确定哪些包或哪些类中的日志记录器,以相同的配置输出日志。
在Spring boot中,已经有三个分组了:root、web和sql。
其中,root日志记录器表示根记录器,作为兜底的选项。
web和sql日志记录器是Spring提供的,分别包括以下包:

参照链接:https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.log-groups
也就是说,org.springframework.jdbc.core包中的类,将会使用sql日志记录器进行输出日志。
在Spring Boot中,使用日志分组,可以改变日志记录级别。
我们可以精确指定某个包或某个类下,日志记录器的级别:
yaml
logging:
level:
root: info # 根日志级别是info
org:
example:
springbootdemo:
LogbackTest: debug # LogbackTest类的日志记录级别是debug
LogTest: info # LogTest类的日志记录级别是info
controller: info # controller包下的所有类的日志记录级别是info如果我们一个个写出每个包或每个类的日志级别,那么配置文件就看起来很冗余了,所以我们可以指定一个分组:
yaml
logging:
level:
root: info # 根日志级别是info
mygroup: trace # 确定该分组的日志级别
org:
example:
springbootdemo:
LogbackTest: debug # LogbackTest类的日志记录级别是debug
group:
mygroup: # 建立分组
- org.example.springbootdemo.controller
- org.example.springbootdemo.LogTest2.3 将日志输出到文件
默认情况下,Spring Boot 只会将日志输出到控制台。要写入日志到文件中,我们需要在application.yml配置文件中设置以下属性之一:
logging.file.name:指定日志文件的名称。可以是绝对路径,也可以是相对于当前目录的相对路径。logging.file.path:指定日志文件将被写入的目录。默认情况下,文件名是spring.log。
优先级:
- 如果同时设置了
logging.file.name和logging.file.path,则只使用logging.file.name。logging.file.path中指定的路径将被忽略。
下表总结了不同的配置及其行为:
| 配置 | 描述 |
|---|---|
| 无 | 只记录到控制台 |
logging.file.name | 写入到指定的文件 |
logging.file.path | 将 spring.log 写入到指定的目录 |
同时设置 logging.file.name 和 logging.file.path | 写入到 logging.file.name 指定的文件,忽略 logging.file.path |
补充说明:
- 默认情况下,日志文件达到 10MB 时会进行滚动(轮换)。
- 与控制台输出一样,默认情况下只记录 ERROR、WARN 和 INFO 级别的消息到文件。
yaml
logging:
file:
name: logs/springboot-demo.log # 日志文件名2.4 日志文件的分割与归档
当使用 Logback 作为日志框架,可以通过application.yml 文件来微调日志分割与归档设置。配置项如下:
logging.logback.rollingpolicy.file-name-pattern:用于创建日志存档的文件名格式。例如,可以设置成logs/springboot-demo.%d{yyyy-MM-dd}.%i.log,这样日志会按照日期归档,文件名包含日期信息,%i表示序号,从零开始。另外,如果文件名以zip或gz结尾,则会自动压缩。logging.logback.rollingpolicy.clean-history-on-start:指定应用程序启动时是否清除历史日志存档。设置为true会在启动时清除,false则保留。logging.logback.rollingpolicy.max-file-size:单个日志文件达到多大尺寸时进行切割存档,默认值是10MB,则当日志文件超过 10MB 就切割成新的文件。logging.logback.rollingpolicy.total-size-cap:所有日志存档文件总大小的容量上限 ,默认值是0B(表示不限制日志文件总大小)。设置为100MB,则当所有存档文件总大小超过 100MB 时,会删除最早的存档文件来腾出空间。logging.logback.rollingpolicy.max-history:最多保留多少历史存档文件。默认值为 7,如果为零,表示不删除一直保留。该项设置与**logging.logback.rollingpolicy.file-name-pattern**密切相关,下面会详细讲解。
注意,以上配置生效的前提是开启了输出日志到文件配置,即指定了日志文件名。
yaml
logging:
file:
name: logs/springboot-demo.log # 日志文件名
logback:
rollingpolicy:
max-file-size: 1MB
max-history: 2
file-name-pattern: logs/springboot-demo.%d{yyyy-MM-dd HH:mm}.%i.log
total-size-cap: 0B
clean-history-on-start: false上面的配置信息表示首先将日志写入logs/springboot-demo.log文件中,如果在日志文件达到了1MB(max-file-size)大小,那么就进行分割,分割后的归档日志文件名为logs/springboot-demo.2024-12-24 10:12.0.log,如果在一分钟之内,日志文件又达到了1MB,那么继续分割,生成的归档日志文件名为logs/springboot-demo.2024-12-24 10:12.1.log,依次类推,直到下一分钟,则序号继续从零开始。
下来来详细讲解一下max-history配置项的作用,该配置项用于删除归档日志文件,与file-name-pattern相关。例如,现在logs/springboot-demo.%d{yyyy-MM-dd HH:mm}.%i.log,那么当需要归档时,首先会进行归档日志文件的创建,然后删除过期的历史归档文件。如何删除呢?首先获取当前时间,例如为2024-12-24 10:20,设置的max-history为2,那么2分钟之前的归档历史文件都会被删除。
如果file-name-pattern设置为logs/springboot-demo.%d{yyyy-MM-dd}.%i.log,并且max-history为10,那么假设日志文件需要归档时的日期为2024-10-30,那么该日期20天之前的归档历史文件都会被删除。
除了在日志需要归档时会进行历史日志清除工作,在程序启动时,如果开启了clean-history-on-start,也会清除历史日志文件。
实现删除逻辑的源码在SizeAndTimeBasedArchiveRemover类的cleanAsynchronously()方法中,一步步跟踪下去:
Details
java
// 启动一个线程去清除历史归档日志文件
public Future<?> cleanAsynchronously(Instant now) {
ArchiveRemoverRunnable runnable = new ArchiveRemoverRunnable(now);
ExecutorService alternateExecutorService = context.getAlternateExecutorService();
Future<?> future = alternateExecutorService.submit(runnable);
return future;
}java
public void clean(Instant now) {
long nowInMillis = now.toEpochMilli();
// for a live appender periodsElapsed is expected to be 1
int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis);
lastHeartBeat = nowInMillis;
if (periodsElapsed > 1) {
addInfo("Multiple periods, i.e. " + periodsElapsed
+ " periods, seem to have elapsed. This is expected at application start.");
}
for (int i = 0; i < periodsElapsed; i++) {
int offset = getPeriodOffsetForDeletionTarget() - i;
Instant instantOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset);
// 清除在目标时间范围内的历史日志文件
cleanPeriod(instantOfPeriodToClean);
}
}java
public void cleanPeriod(Instant instantOfPeriodToClean) {
// 查找在目标时间范围内的文件
File[] matchingFileArray = getFilesInPeriod(instantOfPeriodToClean);
// 找到符合要求的文件,进行删除
for (File f : matchingFileArray) {
checkAndDeleteFile(f);
}
if (parentClean && matchingFileArray.length > 0) {
File parentDir = getParentDir(matchingFileArray[0]);
removeFolderIfEmpty(parentDir);
}
}java
protected File[] getFilesInPeriod(Instant instantOfPeriodToClean) {
// fileNamePattern 就是application.yml配置文件中的file-name-pattern
File archive0 = new File(fileNamePattern.convertMultipleArguments(instantOfPeriodToClean, 0));
// 归档历史日志文件目录
File parentDir = getParentDir(archive0);
// 正则表达式
String stemRegex = createStemRegex(instantOfPeriodToClean);
// 查找目录下符合正则表达式的文件
File[] matchingFileArray = FileFilterUtil.filesInFolderMatchingStemRegex(parentDir, stemRegex);
return matchingFileArray;
}3. 日志XML配置文件
除了在application.yml中配置日志记录器,我们也可以使用logback原始的配置文件进行配置,配置文件名为logback.xml或logback-spring.xml,需要放在resources目录下。现给出一个配置案例:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 将日志写入文件,并归档 -->
<appender name="ROLLINGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/springboot-demo.log</file>
<!-- 注意选取的是 SizeAndTimeBasedRollingPolicy -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 按天归档 -->
<fileNamePattern>logs/archive/springboot-demo-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单文件最大大小 -->
<maxFileSize>1MB</maxFileSize>
<!-- 保留历史天数(因为是按天归档的) -->
<maxHistory>60</maxHistory>
<!-- 日志总文件大小 -->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<!-- 日志内容格式 -->
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} -%kvp -%msg%n</pattern>
</encoder>
</appender>
<!-- 控制台输出日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n</pattern>
</encoder>
</appender>
<appender name="STDOUT2" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>我的格式 ~~~ %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n</pattern>
</encoder>
</appender>
<!-- 定义根日志记录器日志级别 -->
<root level="INFO">
<!-- ref的值需要与appender的name一致 -->
<appender-ref ref="ROLLINGFILE" />
<appender-ref ref="STDOUT" />
</root>
<!-- 定义某个包或某个类的日志级别 -->
<logger name="org.example.springbootdemo.LogTest" level="ERROR" >
<appender-ref ref="STDOUT2" />
</logger>
</configuration>4. 将日志写入到数据库中
4.1 基础使用
我们可以使用logback提供的DBAppender将日记写入数据库,参考链接如下:https://logback.qos.ch/manual/appenders.html#DBAppender
首先,在文档中关注如下这段话:
As of logback version 1.2.8 DBAppender no longer ships with logback-classic. However, DBAppender for logback-classic is available under the following Maven coordinates:
ch.qos.logback.db:logback-classic-db:1.2.11.1
从logback 1.2.8开始,DBAppender不存在于logback-classic包中,如果需要DBAppender,需要我们手动引入以下依赖:
xml
<dependency>
<groupId>ch.qos.logback.db</groupId>
<artifactId>logback-classic-db</artifactId>
<version>1.2.11.1</version>
</dependency>然后,我们从logback-classic/src/main/java/ch/qos/logback/classic/db/script路径下找到SQL脚本文件,我们以PostgreSQL为例:
Details
sql
-- Logback: the reliable, generic, fast and flexible logging framework.
-- Copyright (C) 1999-2010, QOS.ch. All rights reserved.
--
-- See http://logback.qos.ch/license.html for the applicable licensing
-- conditions.
-- This SQL script creates the required tables by ch.qos.logback.classic.db.DBAppender
--
-- It is intended for PostgreSQL databases.
DROP TABLE logging_event_property;
DROP TABLE logging_event_exception;
DROP TABLE logging_event;
DROP SEQUENCE logging_event_id_seq;
CREATE SEQUENCE logging_event_id_seq MINVALUE 1 START 1;
CREATE TABLE logging_event
(
timestmp BIGINT NOT NULL,
formatted_message TEXT NOT NULL,
logger_name VARCHAR(254) NOT NULL,
level_string VARCHAR(254) NOT NULL,
thread_name VARCHAR(254),
reference_flag SMALLINT,
arg0 VARCHAR(254),
arg1 VARCHAR(254),
arg2 VARCHAR(254),
arg3 VARCHAR(254),
caller_filename VARCHAR(254) NOT NULL,
caller_class VARCHAR(254) NOT NULL,
caller_method VARCHAR(254) NOT NULL,
caller_line CHAR(4) NOT NULL,
event_id BIGINT DEFAULT nextval('logging_event_id_seq') PRIMARY KEY
);
CREATE TABLE logging_event_property
(
event_id BIGINT NOT NULL,
mapped_key VARCHAR(254) NOT NULL,
mapped_value VARCHAR(1024),
PRIMARY KEY(event_id, mapped_key),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);
CREATE TABLE logging_event_exception
(
event_id BIGINT NOT NULL,
i SMALLINT NOT NULL,
trace_line VARCHAR(254) NOT NULL,
PRIMARY KEY(event_id, i),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);运行以上SQL语句后,会创建三张表:logging_event 、logging_event_property、logging_event_exception。
然后,在logback.xml文件中配置:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
<driverClass>org.postgresql.Driver</driverClass>
<url>jdbc:postgresql://localhost:5432/数据库名称</url>
<user>账号</user>
<password>密码</password>
</connectionSource>
</appender>
<root level="INFO">
<appender-ref ref="DB"/>
</root>
</configuration>之后,正常写日志就会保存到数据库中:

4.2 配置数据库连接池
我们先来做个小实验,在不配置数据库连接池的前提下向数据库写入1000条日志,记录耗时:
java
@Test
void testLogbackLoggingSystem(){
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
log.warn("日志测试...");
}
long end = System.nanoTime();
System.out.println(end - start);
}耗时为:7053030292
首先引入数据库连接池依赖:
xml
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>6.2.1</version>
</dependency>然后修改logback.xml配置文件:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 配置数据库日志记录器 -->
<appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
<!-- 使用 DataSourceConnectionSource -->
<connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
<!-- 配置数据库连接信息(带数据库连接池) -->
<dataSource class="com.zaxxer.hikari.HikariDataSource">
<driverClass>org.postgresql.Driver</driverClass>
<!-- 注意,此处是jdbcUrl,而不是url -->
<jdbcUrl>jdbc:postgresql://localhost:5432/lhb</jdbcUrl>
<user>lhb</user>
<password>lhb</password>
<!-- 以下是数据库连接池的配置信息 -->
<property name="maximumPoolSize" value="20"/>
<property name="minimumIdle" value="5"/>
<property name="connectionTimeout" value="30000"/>
<property name="idleTimeout" value="600000"/>
<property name="maxLifetime" value="1800000"/>
</dataSource>
</connectionSource>
</appender>
<root level="INFO">
<appender-ref ref="DB"/>
</root>
</configuration>然后我们再次运行测试程序,耗时为:133257167。可以看到效率大大提高了。
5. 异步输出日志
logback提供了异步日志支持,参考链接:https://logback.qos.ch/manual/appenders.html#AsyncAppender
我们可以配置AsyncAppender来启用异步日志:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 配置数据库日志记录器 -->
<appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
<driverClass>org.postgresql.Driver</driverClass>
<url>jdbc:postgresql://localhost:5432/数据库名称</url>
<user>账号</user>
<password>密码</password>
</connectionSource>
</appender>
<!-- 配置异步日志 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 配置实际的日志记录器 -->
<appender-ref ref="DB"/>
</appender>
<root level="INFO">
<!-- 使用异步日志 -->
<appender-ref ref="ASYNC"/>
</root>
</configuration>AsyncAppender 提供了几个重要的配置项,用于控制其行为:
appender-ref: (必需) 指定被包装的 Appender。异步 Appender 最终会将日志事件传递给这个 Appender 进行实际的写入操作。queueSize: (可选,默认值 256) 阻塞队列的大小。当应用程序产生日志的速度超过了异步处理的速度时,日志事件会被放入这个队列中。如果队列满了,默认情况异步日志会阻塞。discardingThreshold: (可选,默认值队列大小的 80%) 当队列中的日志事件数量超过这个阈值时,会开始丢弃TRACE、DEBUG和INFO级别的日志,只保留WARN和ERROR级别的日志。为0表示不丢弃。includeCallerData: (可选,默认值 false) 是否包含调用者数据(例如类名、方法名、行号)。开启此项会轻微降低性能,但可以提供更详细的日志信息。建议开启,否则会丢失数据:
neverBlock: (可选,默认值 false) 设置为 true 时,如果队列已满,则立即丢弃日志事件,而不是阻塞调用线程。这可以最大限度地减少对应用程序性能的影响,但可能会导致更多日志丢失。
例如,完整的配置如下:
xml
<!-- 配置异步日志 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 配置实际的日志记录器 -->
<appender-ref ref="DB"/>
<!-- 设置队列的最大容量 -->
<queueSize>1024</queueSize>
<!-- 设置丢弃日志(TRACE DEBUG INFO)的阈值 0表示不丢弃 -->
<discardingThreshold>0</discardingThreshold>
<!-- 设置是否包含调用者信息 -->
<includeCallerData>true</includeCallerData>
<!-- 设置是否阻塞 false 表示阻塞 -->
<neverBlock>false</neverBlock>
</appender>我们结合异步输出日志和将日志写入数据库,再次运行上面的测试程序,耗时为:14577625。相比于引入数据库连接池,又大大提高了效率。
6. Spring Boot切换日志实现
如果我们想使用log4j2作为日志框架,可以在pom.xml中配置如下:
xml
<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>
<!-- 移除logback -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>