跳转至

日志

log.info、log.error就是打日志,日志就是记录程序运行时的状态和信息,当系统出现问题时,可以通过日志快速定位问题。

日志接口与日志实现

业务代码决定“记录什么”,日志配置决定“哪些要输出、输出到哪里、长什么样、保存多久”。

名称 职责
SLF4J(Simple Logging Facade for Java,Java 简单日志门面) 提供统一的 Logger 接口,让业务代码调用 info()、error() 等方法,不绑定某一种日志实现
Logback、Log4j2 日志的具体实现,负责级别过滤、格式化、控制台和文件输出等;
Lombok 的 @Slf4j 编译时自动生成 SLF4J 的 log 字段,省去手动声明;本身不是日志实现,也不是 Spring 注入的 Bean

常见组合是 @Slf4j → SLF4J → Logback,或者 @Slf4j → SLF4J → Log4j2。接口、适配器和实现可以分成多个 Jar;“只选一种实现”不等于只能有一个日志依赖。SLF4J 官方说明、Lombok 官方说明

Spring Boot 默认使用 Logback,采用默认配置比较省事;Log4j2 提供基于 Disruptor(高效线程间通信组件)的 AsyncLogger,以及减少临时对象分配的调优能力。Log4j2 异步日志说明

MyBatis 的 SQL 日志

MyBatis 框架内置日志工厂,会查找运行环境中可用的日志接口或实现,并把日志委托给它。可以使用:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • JDK logging

在已经集成日志的 Spring Boot 项目中,不需要为了查看 SQL 再引入另一套日志实现。可以在服务的 src/main/resources/application.yml 中只调整目标 Mapper 的级别:

logging:
  level:
    root: INFO
    com.example.mapper: DEBUG

把 com.example.mapper 替换为实际 Mapper 接口的包名,也可以填写单个 Mapper 的完整类名或 XML 的 namespace。如果文件已有 logging:,在原节点下合并,不要新增同名顶层节点。

MyBatis 在 DEBUG 级别记录 SQL,在 TRACE 级别还会记录结果行。用不含真实用户数据的测试查询验证;SQL 参数和结果可能包含密码、Token 或个人信息,不能在生产环境随意开启详细输出。MyBatis 官方说明

Logback

Spring Boot 的常用 Starter 默认引入 Logback。

依赖

<!-- 已有此 Starter 时,不需要重复添加;版本交给 Spring Boot 管理 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

下面的 Logback 示例与后面的 Log4j2 示例是两套可选方案,不要在同一服务里同时启用。Spring Boot 日志集成说明

logback-spring.xml

在选择 Logback 的 Spring Boot 服务中,新建 src/main/resources/logback-spring.xml;框架会识别这个约定名称。

通过修改日志配置文件

  • 可以把日志同时输出到控制台和文件中。
  • 设置日志格式。
  • 控制日志级别。
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 相对于启动工作目录;my-service 替换为实际服务名,各服务单独存放 -->
    <property name="LOG_HOME" value="logs/my-service"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 同时按日期和文件大小归档;.gz 后缀表示压缩归档 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>${LOG_HOME}/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <!-- 按天归档时保留30个历史周期,并限制归档总大小 -->
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <!-- 日志文本编码与格式;压缩由上面的 .gz 后缀启用 -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 可选的异步输出组件:本例只定义它,root 暂未引用 -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 0关闭队列压力下对低级别日志的主动丢弃,不保证崩溃或停机时零丢失 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 队列大小。更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>512</queueSize>
        <!-- 队列满时是否阻塞,false表示会阻塞 -->
        <neverBlock>false</neverBlock>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="FILE"/>
    </appender>

    <logger name="org.springframework.boot" level="INFO"/>

    <!-- 设置日志级别为info -->
    <root level="info">
        <!-- 如果启用 ASYNC,应替换下面的 FILE 引用,避免同一日志重复写入 -->
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

本机启动后,检查控制台与工作目录下的 logs/my-service/application.log。按大小、日期滚动时会产生压缩归档;maxHistory 与 totalSizeCap 作用于归档清理,不是整个磁盘的实时硬配额。

使用

以下是 SLF4J 的通用写法,使用 Logback 或 Log4j2 都适用;Logger 和 LoggerFactory 分别从 org.slf4j 包导入。

1、通过LoggerFactory手动获取Logger日志对象

public class MyService {
    private static final Logger logger = LoggerFactory.getLogger(MyService.class);
    public void doSomething() {
        logger.info("Doing something in MyService");
    }
}

2、使用this.getClass()获取当前类的类型,创建Logger对象

public class MyService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    public void doSomething() {
        logger.info("Doing something in MyService");
    }
}

简单方法

使用Lombok工具库提供的@Slf4j注解,类上面引入日志:@Slf4j,可以自动为当前类生成日志对象,不用手动定义了。

log.info("用户名已存在");
log.error("业务操作失败,异常类型={}", e.getClass().getName());

log.error("业务操作失败", e) 能输出完整异常堆栈,但异常消息及其 cause 可能包含秘密;只有确认内容安全或已脱敏时才使用。只输出 e.getMessage() 不等于脱敏,它还会丢失堆栈定位信息。日志敏感信息边界

日志分级

  • trace(跟踪信息):最细粒度的信息,通常只在开发过程中使用,用于跟踪程序的执行路径。
  • debug(调试信息):记录程序运行时的内部状态和变量值。
  • info(普通信息) :记录系统的关键运行状态和业务流程。
  • warn(警告信息) :表示可能存在潜在问题,但系统仍可继续运行。
  • error(错误信息) :表示出现了影响系统功能的问题,需要及时处理。

SLF4J 的 Logger 没有 fatal() 方法;Log4j2 自有 API 支持 FATAL,使用 @Slf4j 时不要照搬为 log.fatal()。SLF4J Logger API

如果所有信息都用同一级别,出了问题时不容易快速找到错误信息。

在生产环境通常会把日志级别调高,这样debug级别的日志就不会输出了。防止重要信息被无用日志淹没

记录日志信息

log.info("任务处理完成,数量={}", count); // count 为本次完成的任务数量

上面的{}叫参数化日志,{}是一个占位符,日志框架会在运行时自动把后面的参数值替换进去。

利用 AOP(Aspect-Oriented Programming,面向切面编程),可以给选定方法的执行前后添加统一日志。Spring AOP 只拦截匹配切点且经过代理的调用,不能保证覆盖每次调用;不要默认记录全部参数和返回值,以免泄露秘密或产生过量日志。

统一日志格式

在日志配置文件中定义统一日志格式,包含时间戳、线程名称、日志级别,类名、方法名、具体内容等关键信息。

还可以通过 MDC(Mapped Diagnostic Context,映射诊断上下文)给日志添加额外信息,例如不含秘密的请求 ID,方便把同一次请求的多条日志关联起来。在线程池中使用时,请求结束后要清理;异步任务或响应式流程需要正确传播上下文,不能假定线程切换后仍自动保留。MDC 官方说明

异步日志

同步日志由调用线程执行日志处理和输出,可能等待 I/O(输入输出);写入缓冲区不等于已经持久化到磁盘。异步日志通常先把事件放入队列,再由后台线程输出,可以降低调用线程的等待,但不是任何情况下都更快。

队列满时,可能根据配置阻塞调用线程或丢弃日志。Logback 的 AsyncAppender 默认在剩余容量不足约 20% 时丢弃 TRACE、DEBUG、INFO;discardingThreshold=0 关闭该主动丢弃策略,搭配 neverBlock=false 时,队列满会等待。Logback 异步日志说明

异步日志缺点:如果程序突然崩溃,缓冲区中还没来得及写入文件的日志可能会丢失。

日志管理

自动清理日志,防止占过多空间。

Logback 的时间、大小和历史归档限制见前面的完整配置;Log4j2 的归档与删除见下面的实操示例。不要在多个章节维护同一份 XML。

不同服务、同一服务的不同实例应避免写入同一个文件。日志目录要有合适的权限,日志文件不提交到 Git;归档策略不能替代磁盘容量监控。

集成日志收集系统

对于分布式系统,可以使用集中日志收集系统,例如 ELK:Elasticsearch 负责存储与检索,Logstash 负责采集处理,Kibana 负责查询和展示。它解决跨服务查日志的问题,不替代应用内的 SLF4J、Logback 或 Log4j2。

Spring Boot 集成 Log4j2:从依赖到文件输出

实现效果与前置准备

目标是:控制台和文件同时输出、默认记录 INFO 及以上、按天或约 10 MB 归档,并在滚动时清理达到 14 天的归档文件。

第一步:引入实现,并排除默认日志

在需要启动的服务 pom.xml 的 <dependencies> 中配置下面两项;原有业务依赖不变。

<!-- 从这个依赖路径排除默认日志 Starter,避免引入 Logback -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 引入 Log4j2 实现及 SLF4J 到 Log4j2 的适配器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

Starter 是一组配套依赖的入口。上面的 Log4j2 Starter 会引入 log4j-core(具体实现)和 log4j-slf4j2-impl(把 SLF4J 调用转交给 Log4j2 的适配器)等组件,不需要再分别手写这些依赖的版本。Spring Boot 集成说明、Log4j2 适配器说明

在 IDEA 重新加载 Maven 项目后,从学习应用含 pom.xml 的目录检查完整日志依赖树:

mvn dependency:tree '-Dincludes=org.springframework.boot:spring-boot-starter-logging,org.apache.logging.log4j:*,ch.qos.logback:*,org.slf4j:*'

检查是否有 log4j-core、log4j-slf4j2-impl,并确认未混入 logback-classic、反向适配器 log4j-to-slf4j 或其他 SLF4J 实现。多模块项目在聚合根目录执行,并追加 -pl 服务模块的相对路径 -am,路径从根 POM 的模块配置确认。

exclusions 只影响对应的依赖路径,不是全局禁用。如果其他依赖或测试依赖仍带入 Logback,应沿依赖树找到引入位置,再对相应依赖补充排除;不要看到某一处已排除,就认定整个应用已经统一。

第二步:创建 log4j2-spring.xml

在该服务的 src/main/resources/ 下新建 log4j2-spring.xml。Spring Boot 会自动识别这个名称,不需要再写 Java 配置类读取它;只有使用其他位置时才需要通过 logging.config 指定。Logback 与 Log4j2 的 XML 格式不同,不能只给旧文件改名。Spring Boot 配置加载说明

先理解配置中四个部分的职责:

配置部分 作用
Properties 定义日志目录、格式等复用值
Appenders 定义输出目标,例如控制台、滚动文件;Appender 可理解为日志输出器
PatternLayout 定义一条日志的文本格式
Loggers 配置日志级别,并引用具体输出器;Root 是默认根 Logger

将以下完整配置放入新文件,把 logs/my-service 中的 my-service 替换为这个服务的名称,可以从 spring.application.name 确认。这里是普通目录文本,不会自动读取服务名。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Properties>
        <!-- 相对于应用启动工作目录,不是相对于本 XML 所在目录 -->
        <Property name="LOG_DIR">logs/my-service</Property>
        <!-- 时间、级别、线程、Logger 名称、内容、换行 -->
        <Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %logger - %msg%n</Property>
    </Properties>

    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
        </Console>

        <RollingFile name="File"
                     fileName="${LOG_DIR}/application.log"
                     filePattern="${LOG_DIR}/archive/application-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
            <Policies>
                <!-- 按天滚动;跨日后的日志事件触发检查 -->
                <TimeBasedTriggeringPolicy interval="1"/>
                <!-- 同一天日志较多时,再按约 10 MB 拆分 -->
                <SizeBasedTriggeringPolicy size="10 MB"/>
            </Policies>
            <!-- 不按序号限制归档数量,使用下面的条件清理 -->
            <DefaultRolloverStrategy fileIndex="nomax">
                <Delete basePath="${LOG_DIR}/archive" maxDepth="1">
                    <!-- 只删除本目录中指定格式、修改时间达到 14 天的归档 -->
                    <IfFileName glob="application-*.log.gz">
                        <IfLastModified age="P14D"/>
                    </IfFileName>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
    </Appenders>

    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>
</Configuration>

%d 是时间,%-5level 是左对齐的级别,%t 是线程名,%logger 通常为记录日志的类名,%msg 是消息,%n 是换行。${LOG_DIR} 和 ${LOG_PATTERN} 引用上面定义的属性。仅定义 Appender 不代表会输出,AppenderRef 必须引用它。Log4j2 配置说明

一条 log.info() 的过程是:SLF4J 调用经过适配器交给 Log4j2,由有效日志级别决定是否接收,再按格式交给两个输出器。未配置子 Logger 或外部级别覆盖时,上面的 INFO 根级别会隐藏 DEBUG、TRACE,但保留 INFO、WARN、ERROR。

第三步:写几条不含秘密的日志,验证输出

在学习应用的启动类所在包内,新建 LoggingDemo.java。把下面的 com.example 替换为启动类的实际包名;放在这个包里,Spring Boot 才能按默认扫描范围发现它。

package com.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

/** 仅用于本地学习:应用启动完成后打印固定内容,验证日志配置。 */
@Component
public class LoggingDemo implements ApplicationRunner {
    private static final Logger log = LoggerFactory.getLogger(LoggingDemo.class);

    @Override
    public void run(ApplicationArguments args) {
        log.debug("LOGGING_DEMO_DEBUG");
        log.info("LOGGING_DEMO_INFO");
        log.warn("LOGGING_DEMO_WARN");
    }
}

ApplicationRunner 是 Spring Boot 在应用启动完成阶段调用的接口;这里用它生成测试日志,不需要新增 HTTP 接口,也不依赖 Lombok。学习结束后移除这个测试类,不要把它当成业务功能。

在 IDEA 中重新运行启动类,检查:

  1. Run 控制台能看到两条 INFO、WARN 测试消息,没有 DEBUG 测试消息。
  2. 在运行配置的 Working directory(工作目录)下找到 logs/实际服务名/application.log,文件中也有这两条消息;仅创建配置文件不会自动给每个接口添加业务日志。
  3. 如果两处都没出现测试消息,先看应用是否启动成功、测试类是否被扫描;仅文件没有输出时,检查目录权限、实际工作目录、Appender 引用以及启动时的日志配置错误。
  4. 同时检查现有 application.yml、环境变量、启动参数是否覆盖了 logging.level 或 logging.config。有 Nacos 等外部配置时也要核对,但不要把配置中的秘密复制到日志或教程。

Spring Boot 会在启动早期根据可用依赖初始化日志系统;即使没有自定义 XML,也有默认控制台配置。因此,“控制台有日志”本身不能证明这个文件已生效,文件输出和级别检查要一起验证。

第四步:理解并检查滚动清理

  • application.log 是当前写入文件;归档文件放入 archive/,名称包含日期、序号 %i,以 .gz 压缩。
  • 时间和大小条件任一满足即可滚动。时间策略依赖日志事件:跨过日期边界后,由后续写入触发,不是午夜独立执行的定时任务。
  • fileIndex="nomax" 不按序号淘汰文件,但不禁用下面显式配置的 Delete。
  • P14D 表示 14 天,按文件最后修改时间判断。maxDepth="1" 只检查指定归档目录的直接子项;文件名条件与时间条件必须同时满足。
  • 清理在滚动后执行,不保证文件满 14 天立刻消失,也不限制日志总容量。应用停止、没有新日志触发滚动或清理动作失败时,旧文件可能继续存在。滚动与删除官方说明

本机验证不必等待 10 MB 或 14 天:在独立学习应用中,把 LOG_DIR 临时改为 logs/logging-demo-check、大小阈值改为 1 KB,并在测试类的 run() 中临时加入:

for (int index = 0; index < 200; index++) {
    log.info("LOGGING_DEMO_ROLLOVER index={}", index);
}

重新启动,检查测试目录出现 .log.gz 归档。若还要验证清理,只在这个专用测试目录中将 age 临时改为 PT1S(1 秒),生成归档后等待超过 1 秒,再重新启动产生新日志触发滚动,检查符合条件的旧归档被清理。不要用真实业务日志做短保留期实验。

验证结束后恢复目录、10 MB 和 P14D,移除循环及测试类。服务器或容器部署还需要明确持久化目录、权限、实例隔离和容量监控,不能直接把本机相对路径当作部署方案。

这份配置使用同步业务日志,没有启用 AsyncLogger 或 AsyncAppender;文件压缩和清理可以在后台执行,不等于业务日志调用已经改成异步。先把依赖、输出、归档验证正确,再按实际性能需要选择异步方案。