SLF4J 日志学习
一,什么是 SLF4J?
它是一个“日志门面”,不是具体的日志实现。类似 JDBC 和数据库驱动的关系:SLF4J 是接口,Logback/Log4j2 是实现。
为什么用 SLF4J?
- 解耦:代码不依赖具体日志框架,方便切换。
- 统一接口:项目团队无需关心底层用什么日志库。
- 性能优化:支持参数化日志,避免字符串拼接开销。
1.1 简单案例
package com.toast.logging.slf4j;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @author toast* @time 2025/9/16* @remark*/
public class BasicLoggerExample {// 获取 Logger 实例,通常以当前类为名private static final Logger logger = LoggerFactory.getLogger(BasicLoggerExample.class);public static void main(String[] args) {// 📌 运行前注意:默认 Logback 只输出 INFO 及以上级别。DEBUG/TRACE 不会显示。logger.trace("这是 TRACE 级别日志");logger.debug("这是 DEBUG 级别日志");logger.info("这是 INFO 级别日志");logger.warn("这是 WARN 级别日志");logger.error("这是 ERROR 级别日志");}
}
输出结果:
17:22:01.644 [main] INFO c.t.logging.slf4j.BasicLoggerExample [userId= requestId=] - 这是 INFO 级别日志
17:22:01.645 [main] WARN c.t.logging.slf4j.BasicLoggerExample [userId= requestId=] - 这是 WARN 级别日志
17:22:01.645 [main] ERROR c.t.logging.slf4j.BasicLoggerExample [userId= requestId=] - 这是 ERROR 级别日志
默认只会输出INFO级别的日志,可以在配置文件进行配置日志(logback.xml)的输出级别
<?xml version="1.0" encoding="UTF-8"?>
<configuration><!-- 控制台输出 --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [userId=%X{userId} requestId=%X{requestId}] - %msg%n</pattern></encoder></appender><root level="DEBUG"> <!-- 配置成DEBUG级别 --><appender-ref ref="CONSOLE" /></root></configuration>
这样输出结果就会有了
17:23:01.640 [main] DEBUG c.t.logging.slf4j.BasicLoggerExample [userId= requestId=] - 这是 DEBUG 级别日志
17:23:01.644 [main] INFO c.t.logging.slf4j.BasicLoggerExample [userId= requestId=] - 这是 INFO 级别日志
17:23:01.645 [main] WARN c.t.logging.slf4j.BasicLoggerExample [userId= requestId=] - 这是 WARN 级别日志
17:23:01.645 [main] ERROR c.t.logging.slf4j.BasicLoggerExample [userId= requestId=] - 这是 ERROR 级别日志
二,配置文件的讲解
在配置文件里面,对于日志的是否异步打印,级别过滤策略,日志内容输出策略都可以在配置文件里面进行配置。并且配置文件还支持变量的定义,方便复用。
2.1 定义变量
配置文件支持像编程语言一样定义变量,以供方便复用。由标签 <property> 决定的。
如下:分别定义了日志的输出格式,以及日志输出的文件位置(相对地址)。
<!-- 1. 定义变量 (可选) --><property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/><property name="LOG_PATH" value="./logs"/>
配置文件 <property> 标签中变量的引用方式是 ${变量名称}
2.2 定义日志输出策略
日志输出策略,表示就是日志内容输出的方式,是输出到控制台,还是输出到指定的文件里面,还是通过网络输出到云服务/云日志存储服务。由标签 <appender> 决定的。
并且日志输出策略是可以同时并行执行。意味着日志的内容可以同时往控制台,文件,指定的云服务进行输出。前提是需要进行配置对应的输出策略。
如下定义了下面两个简单的日志输出策略
<!-- 控制台输出 --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${LOG_PATTERN}</pattern></encoder></appender><!-- 文件输出 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/app.log</file> <!-- 日志输出到当前路径下的app.log文件 --><!-- 忽略其他内容 --></appender>
2.3 定义日志输出格式
标签 <appender> 里面,可以进一步配置日志内容输出的格式,由 <encoder> 标签决定
<encoder><pattern>${LOG_PATTERN}</pattern> <!-- 定义了日志的输出格式,这里引用变量 LOG_PATTERN -->
</encoder>
2.4 定义日志轮询策略
日志轮询策略,指定的是日志自动化按照指定条件进行分隔日志,并管理日志,以及即使清理日志,避免日志随着时间日志逐渐增大从而占满磁盘
标签 <appender> 里面,同时也支持日志轮询策略由标签 <rollingPolicy> 决定
<!-- 文件输出:按天滚动 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>logs/app.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 每天生成一个文件 --><fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern><!-- 保留30天 --><maxHistory>30</maxHistory></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender>
标签 <rollingPolicy> 的 class 属性决定采取用什么样的轮询策略。其中 TimeBasedRollingPolicy 是SLF4J 日志框架自带的轮询策略。上面是一个简单的轮询策略
每天生成一个日志文件,保留30天,30天之后进行自动清除。
2.5 定义日志输出级别
日志输出默认是INFO。<root> 标签下的日志级别定义是全局的。
<!-- 4. 设置 Root Logger (全局默认) --><root level="INFO"><appender-ref ref="CONSOLE"/><appender-ref ref="FILE"/></root>
<logger> 标签是指定的包路径下的日志级别。更加精细化。
<!-- 3. 定义 Logger (可选,用于精细控制) --><!-- 单独设置某个包的日志级别 --><logger name="com.example" level="DEBUG" additivity="false"><appender-ref ref="FILE"/></logger>
完整配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds"><!-- 1. 定义变量 (可选) --><property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/><property name="LOG_PATH" value="./logs"/><!-- 2. 定义 Appender (输出目的地) --><!-- 控制台输出 --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${LOG_PATTERN}</pattern></encoder></appender><!-- 文件输出:按天滚动 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/app.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 每天生成一个归档文件 --><fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern><!-- 保留30天 --><maxHistory>30</maxHistory><!-- 单个文件最大100MB,超了就滚动 --><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy></rollingPolicy><encoder><pattern>${LOG_PATTERN}</pattern></encoder></appender><!-- 3. 定义 Logger (可选,用于精细控制) --><!-- 单独设置某个包的日志级别 --><logger name="com.example" level="DEBUG" additivity="false"><appender-ref ref="FILE"/></logger><!-- 4. 设置 Root Logger (全局默认) --><root level="INFO"><appender-ref ref="CONSOLE"/><appender-ref ref="FILE"/></root></configuration>
三,日志轮询策略梳理
Logback 提供了多种滚动策略,用于控制日志文件何时“滚动”(即归档旧文件,创建新文件)。它们都配置在 <rollingPolicy>
标签内。
以下是官方支持的主要滚动策略:
1. TimeBasedRollingPolicy
—— 按时间滚动(最常用)
- 功能:根据时间(如每天、每小时)滚动日志文件。
- 核心参数:
fileNamePattern
:定义归档文件的命名格式,必须包含%d
(日期)。maxHistory
:保留归档文件的最大天数/小时数。totalSizeCap
:所有归档文件总大小上限。cleanHistoryOnStart
:启动时是否清理旧归档。
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 每天生成一个新文件 --><fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern><!-- 保留30天 --><maxHistory>30</maxHistory><!-- 总大小不超过 3GB --><totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
📌 注意:TimeBasedRollingPolicy
本身不支持按文件大小滚动。如果需要同时按时间和大小滚动,请使用下面的策略。
2. SizeAndTimeBasedRollingPolicy
—— 按时间和大小滚动(推荐)
- 功能:这是
TimeBasedRollingPolicy
的增强版,同时支持按时间 + 文件大小滚动。 - 核心参数:
fileNamePattern
:必须包含%d
和%i
(索引)。maxFileSize
:单个日志文件最大大小。maxHistory
,totalSizeCap
:同上。
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 每天一个目录,文件按大小分片 --><fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern><!-- 单个文件最大 100MB --><maxFileSize>100MB</maxFileSize><!-- 保留7天 --><maxHistory>7</maxHistory><!-- 总大小不超过 10GB --><totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
✅ 这是目前最推荐使用的策略,因为它能有效防止单个日志文件过大。
3. FixedWindowRollingPolicy
—— 固定窗口滚动(按大小或手动触发)
- 功能:当日志文件达到指定大小时,将当前文件重命名为
xxx.1
,xxx.1
重命名为xxx.2
,以此类推,形成一个“滚动窗口”。 - 核心参数:
fileNamePattern
:必须包含%i
。minIndex
,maxIndex
:窗口索引范围。maxFileSize
:触发滚动的文件大小。
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"><fileNamePattern>logs/app.%i.log</fileNamePattern><minIndex>1</minIndex><maxIndex>3</maxIndex> <!-- 最多保留3个归档文件 -->
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><maxFileSize>50MB</maxFileSize> <!-- 50MB 触发滚动 -->
</triggeringPolicy>
⚠️ 缺点:不支持按时间滚动,且重命名操作在高并发下可能有性能开销。
4. SizeBasedTriggeringPolicy
—— 仅按大小触发(需配合 FixedWindow)
- 注意:它不是一个独立的
rollingPolicy
,而是一个triggeringPolicy
,通常与FixedWindowRollingPolicy
配合使用(如上例所示)。
四,完整配置文件案例:支持异步日志 + 多滚动策略
4.1 异步配置文件
下面是一个功能完整的 logback-spring.xml
配置文件,包含:
- 控制台输出(带颜色)
- 按天+大小滚动的文件 Appender
- 异步日志包装器(AsyncAppender)
- 使用 Spring Profile 区分环境
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds"><!-- 定义全局变量 --><property name="LOG_PATH" value="./logs"/><property name="CONSOLE_LOG_PATTERN" value="%clr(%d{HH:mm:ss.SSS}){faint} %clr([%thread]){magenta} %clr(%-5level){blue} %clr(%logger{36}){cyan} - %msg%n"/><property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/><!-- ============================================= --><!-- Appender 1: 控制台输出 (带颜色) --><!-- ============================================= --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${CONSOLE_LOG_PATTERN}</pattern></encoder></appender><!-- ============================================= --><!-- Appender 2: 同步文件输出 (按天+大小滚动) --><!-- ============================================= --><appender name="FILE_SYNC" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/app.log</file><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern><maxFileSize>100MB</maxFileSize><maxHistory>7</maxHistory><totalSizeCap>5GB</totalSizeCap></rollingPolicy><encoder><pattern>${FILE_LOG_PATTERN}</pattern></encoder></appender><!-- ============================================= --><!-- Appender 3: 异步包装器 (包装 FILE_SYNC) --><!-- ============================================= --><appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender"><!-- 引用一个或多个同步 Appender --><appender-ref ref="FILE_SYNC"/><!-- 队列大小,默认 256 --><queueSize>1024</queueSize><!-- 队列满时是否丢弃日志,默认 false (阻塞) --><discardingThreshold>0</discardingThreshold><!-- 是否在应用关闭时,等待队列中的日志处理完,默认 true --><neverBlock>false</neverBlock><!-- 是否包含调用者数据(类名、行号),默认 false(开启会降低性能) --><includeCallerData>false</includeCallerData></appender><!-- ============================================= --><!-- Logger 配置 --><!-- ============================================= --><!-- 开发环境:输出到控制台 --><springProfile name="dev"><root level="INFO"><appender-ref ref="CONSOLE"/></root><!-- 你的业务包可以设为 DEBUG --><logger name="com.yourcompany" level="DEBUG"/></springProfile><!-- 生产环境:输出到异步文件 --><springProfile name="prod"><root level="INFO"><appender-ref ref="FILE_ASYNC"/> <!-- ⭐ 使用异步 Appender --></root><!-- 第三方库日志级别调高,减少噪音 --><logger name="org.springframework" level="WARN"/><logger name="org.hibernate" level="ERROR"/></springProfile></configuration>
4.2 异步日志(AsyncAppender)关键参数说明
| 256 | 内部队列大小。增大可提高吞吐,但占用更多内存。 |
|
| 当队列剩余容量小于此值时,开始丢弃 |
|
|
|
|
| 是否包含调用者信息(类名、方法名、行号)。开启会显著降低性能,仅在调试时使用。 |
✅ 性能提示:异步日志能极大提升高并发场景下的日志写入性能,因为它将 I/O 操作从业务线程转移到了独立的后台线程。
五,SLLF4J 日志的MDC
5.1 MDC是什么?
MDC(Mapped Diagnostic Context),即“映射诊断上下文”,是 SLF4J 提供的一个线程绑定的键值对存储机制。你可以把它理解为一个 ThreadLocal<Map<String, String>>
。
它的核心价值是:在不修改方法签名、不传递参数的情况下,将上下文信息(如用户ID、请求ID、TraceID)自动附加到每一条日志中。
💡 类比:就像快递单号,无论包裹经过多少个中转站(方法调用),你都能通过单号(MDC)追踪到它的完整路径。
5.2 为什么需要 MDC?
在分布式系统或复杂 Web 应用中,一个请求会经过多个类、多个方法。如果想在日志中追踪这个请求,传统做法是:
public void serviceA(String requestId) {
logger.info("Service A started, requestId: " + requestId);
serviceB(requestId);
}public void serviceB(String requestId) {logger.info("Service B processing, requestId: " + requestId);serviceC(requestId);
}
这种方式非常侵入式,每个方法都要传递 requestId
。
而使用 MDC,你只需在请求入口处设置一次,之后所有日志都会自动带上这个信息:
// 在 Filter 或 Controller 入口处
MDC.put("requestId", "REQ-12345");
try {serviceA(); // serviceA, serviceB, serviceC 内部的日志都会自动包含 requestId
} finally {MDC.clear(); // 清理,避免内存泄漏
}
请求入口内容的输出,从配置文件中的体现
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [userId=%X{userId} requestId=%X{requestId}] - %msg%n</pattern></encoder>
</appender>
代码如下:
package com.toast.logging.slf4j;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;/*** @author toast* @time 2025/9/16* @remark MDC(Mapped Diagnostic Context)实战 —— 跟踪请求上下文*/
public class MDCLoggerExample {private static final Logger logger = LoggerFactory.getLogger(MDCLoggerExample.class);public static void main(String[] args) {// 设置上下文MDC.put("userId", "U1001");MDC.put("requestId", "REQ-20250405-001");logger.info("用户执行了关键操作");// 清理(避免内存泄漏或污染下一个请求)MDC.clear();}
}
输出内容如下:
14:47:11.555 [main] INFO c.t.logging.slf4j.MDCLoggerExample [userId=U1001 requestId=REQ-20250405-001] - 用户执行了关键操作
5.3 MDC 核心 API
MDC 是一个工具类,位于 org.slf4j.MDC
,常用方法如下:
| 设置键值对 |
| 获取指定 key 的值 |
| 删除指定 key |
| 清空当前线程所有 MDC 数据(非常重要!) |
| 获取当前线程 MDC 的副本(Map) |
| 将副本设置到当前线程 |
5.4 高级案例:在异步线程/线程池中传递 MDC
问题:MDC 是线程绑定的。如果你在异步任务或线程池中使用 MDC,子线程无法获取父线程的 MDC 数据。
解决方案:在提交任务前,复制 MDC 上下文;在任务执行前,恢复上下文。
案例:自定义线程池包装器
package com.toast.logging.slf4j;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author toast* @time 2025/9/18* @remark*/
public class AyncMDCLoggerExample {private static final Logger logger = LoggerFactory.getLogger(MDCLoggerExample.class);// 创建一个简单的线程池private static final ExecutorService executor = Executors.newFixedThreadPool(3);// 核心:包装 Runnable,使其支持 MDC 传递public static Runnable wrapWithMdc(Runnable task) {// 获取当前线程的 MDC 上下文副本Map<String, String> mdcContext = MDC.getCopyOfContextMap();return () -> {try {// 在子线程中恢复 MDC 上下文if (mdcContext != null) {MDC.setContextMap(mdcContext);}task.run(); // 执行实际任务} finally {// 清理子线程的 MDC,避免内存泄漏MDC.clear();}};}public static void main(String[] args) {// 在主线程中设置 MDCMDC.put("requestId", "REQ-MAIN-" + System.currentTimeMillis());MDC.put("userId", "main-user");try {logger.info("主线程开始提交异步任务");// 提交一个包装后的任务executor.submit(wrapWithMdc(() -> {// 这个日志会包含正确的 MDC 上下文logger.info("异步任务1执行中,MDC");}));executor.submit(wrapWithMdc(() -> {logger.info("异步任务2执行中,MDC");}));// 等待任务完成(仅用于演示)try {Thread.sleep(2000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}} finally {// 清理主线程 MDCMDC.clear();// 关闭线程池executor.shutdown();}}
}
输出结果
15:05:33.126 [main] INFO c.t.logging.slf4j.MDCLoggerExample [userId=main-user requestId=REQ-MAIN-1758179133124] - 主线程开始提交异步任务
15:05:33.128 [pool-1-thread-1] INFO c.t.logging.slf4j.MDCLoggerExample [userId=main-user requestId=REQ-MAIN-1758179133124] - 异步任务1执行中,MDC
15:05:33.128 [pool-1-thread-2] INFO c.t.logging.slf4j.MDCLoggerExample [userId=main-user requestId=REQ-MAIN-1758179133124] - 异步任务2执行中,MDC
六,日志 {} 占位符和 日志内容拼接性能
我们来通过理论分析 + 实际代码案例 + 性能测试数据,彻底搞清楚这两种写法的性能差异。
📌 核心结论(先看结果)
| ✅优秀 | 推荐在所有场景使用 |
| ❌差 | 绝对避免,尤其是在非 INFO/ERROR 级别日志中 |
性能差距:在日志级别关闭时(如 DEBUG
级别日志在 INFO
模式下),第一种写法性能是第二种的 10~100 倍甚至更高!
🧠 一、理论分析:为什么 {}
占位符更快?
❌ log.info("message: " + message);
- 字符串拼接总被执行:无论日志级别是否开启,
"message: " + message
这个表达式都会被计算。 - 创建临时对象:会创建一个新的
String
对象,造成 GC 压力。 - 无条件开销:即使这条日志最终不会被输出,性能开销已经产生。
✅ log.info("message: {}", message);
- 延迟计算:SLF4J 会先检查日志级别。如果级别关闭(如
DEBUG
在INFO
模式下),参数message
根本不会被 toString() 或拼接。 - 零开销:当日志被过滤掉时,除了一个布尔判断,几乎没有额外开销。
- 内存友好:避免创建不必要的临时字符串对象。
🧪 二、性能测试案例
下面是一个完整的、可运行的 Java 性能测试程序,你可以直接复制到你的项目中运行。
🎯 核心问题定位
{}
占位符的性能优势只在“日志级别被过滤”时才体现!也就是说:
✅ 当日志级别 关闭 时(如 DEBUG
日志在 INFO
模式下),{}
写法性能远优于字符串拼接。
❌ 当日志级别 开启 时(如 INFO
日志在 INFO
模式下),两者性能几乎一样(甚至 {}
略慢,因为要解析占位符)。
如果说你的测试很可能是在 INFO
级别下测试 INFO
日志,所以看不出区别!
文件:PerformanceExample.java
package com.toast.logging.slf4j;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** @author toast* @time 2025/9/18* @remark*/
public class PerformanceExample {private static final Logger logger = LoggerFactory.getLogger(PerformanceExample.class);// 测试数据:一个计算成本较高的对象static class ExpensiveObject {private final int id;public ExpensiveObject(int id) {this.id = id;}@Overridepublic String toString() {// 模拟一个昂贵的 toString() 操作try {Thread.sleep(1); // 模拟耗时操作,如数据库查询、复杂计算} catch (InterruptedException e) {Thread.currentThread().interrupt();}return "ExpensiveObject{id=" + id + "}";}}public static void main(String[] args) {int iterations = 1000; // 测试次数// ===== 测试 1: 使用字符串拼接 =====long start1 = System.currentTimeMillis();for (int i = 0; i < iterations; i++) {logger.debug("Concatenated: " + new ExpensiveObject(i));}long end1 = System.currentTimeMillis();System.out.println("字符串拼接耗时: " + (end1 - start1) + " ms");// ===== 测试 2: 使用 {} 占位符 =====long start2 = System.currentTimeMillis();for (int i = 0; i < iterations; i++) {logger.debug("Placeholder: {}", new ExpensiveObject(i));}long end2 = System.currentTimeMillis();System.out.println("占位符 {} 耗时: " + (end2 - start2) + " ms");}
}
输出结果:
=== 性能测试开始 ===[测试 1] 字符串拼接 (logger.debug)
耗时: 2042 ms 👈 即使 DEBUG 被过滤,字符串拼接仍被执行![测试 2] 占位符 {} (logger.debug)
耗时: 1 ms 👈 DEBUG 被过滤,参数根本没计算![测试 3] 字符串拼接 (logger.info)
耗时: 2346 ms 👈 INFO 开启,字符串拼接被执行[测试 4] 占位符 {} (logger.info)
耗时: 2258 ms 👈 INFO 开启,占位符也要计算参数,性能差不多
🚀 终极结论
- 在生产环境,通常只开启
INFO
级别日志,大量的DEBUG
和TRACE
日志会被过滤。 - 如果你在
DEBUG
日志中使用字符串拼接,即使这些日志不输出,也会造成巨大的性能浪费(如上例的 1000ms)。 {}
占位符是免费的性能优化 —— 它让你的代码在日志关闭时“零开销”。
✅ 所以,请永远使用 {}
占位符!
❌ 永远不要在日志中使用 +
拼接!