Skip to content

在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-slf4jjul-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.logging API 记录的日志事件重定向到 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日志...

可以看到tracedebug日志没有输出。

2.2 分组记录

所谓分组,就是确定哪些包或哪些类中的日志记录器,以相同的配置输出日志。

在Spring boot中,已经有三个分组了:rootwebsql

其中,root日志记录器表示根记录器,作为兜底的选项。

websql日志记录器是Spring提供的,分别包括以下包:

image-20241222162953557

参照链接: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.LogTest

2.3 将日志输出到文件

默认情况下,Spring Boot 只会将日志输出到控制台。要写入日志到文件中,我们需要在application.yml配置文件中设置以下属性之一:

  • logging.file.name:指定日志文件的名称。可以是绝对路径,也可以是相对于当前目录的相对路径。

  • logging.file.path:指定日志文件将被写入的目录。默认情况下,文件名是 spring.log

优先级:

  • 如果同时设置了 logging.file.namelogging.file.path,则只使用 logging.file.namelogging.file.path 中指定的路径将被忽略。

下表总结了不同的配置及其行为:

配置描述
只记录到控制台
logging.file.name写入到指定的文件
logging.file.pathspring.log 写入到指定的目录
同时设置 logging.file.namelogging.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表示序号,从零开始。另外,如果文件名以zipgz结尾,则会自动压缩。
  • 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.xmllogback-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>

之后,正常写日志就会保存到数据库中:

image-20241222202514517

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%) 当队列中的日志事件数量超过这个阈值时,会开始丢弃 TRACEDEBUGINFO 级别的日志,只保留 WARNERROR 级别的日志。为0表示不丢弃。

  • includeCallerData (可选,默认值 false) 是否包含调用者数据(例如类名、方法名、行号)。开启此项会轻微降低性能,但可以提供更详细的日志信息。建议开启,否则会丢失数据:

    image-20241222213356454

  • 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>