日志¶
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 中重新运行启动类,检查:
- Run 控制台能看到两条
INFO、WARN测试消息,没有DEBUG测试消息。 - 在运行配置的
Working directory(工作目录)下找到logs/实际服务名/application.log,文件中也有这两条消息;仅创建配置文件不会自动给每个接口添加业务日志。 - 如果两处都没出现测试消息,先看应用是否启动成功、测试类是否被扫描;仅文件没有输出时,检查目录权限、实际工作目录、Appender 引用以及启动时的日志配置错误。
- 同时检查现有
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;文件压缩和清理可以在后台执行,不等于业务日志调用已经改成异步。先把依赖、输出、归档验证正确,再按实际性能需要选择异步方案。