当前位置: 首页 > news >正文

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.1xxx.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)关键参数说明

queueSize

256

内部队列大小。增大可提高吞吐,但占用更多内存。

discardingThreshold

queueSize / 5

当队列剩余容量小于此值时,开始丢弃TRACE,DEBUG,INFO级别日志。设为0禁用丢弃。

neverBlock

false

true表示队列满时丢弃日志(不阻塞);false表示阻塞调用线程直到队列有空位。生产环境建议设为 true 避免阻塞业务线程。

includeCallerData

false

是否包含调用者信息(类名、方法名、行号)。开启会显著降低性能,仅在调试时使用。

性能提示:异步日志能极大提升高并发场景下的日志写入性能,因为它将 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,常用方法如下:

MDC.put(String key, String value)

设置键值对

MDC.get(String key)

获取指定 key 的值

MDC.remove(String key)

删除指定 key

MDC.clear()

清空当前线程所有 MDC 数据(非常重要!

MDC.getCopyOfContextMap()

获取当前线程 MDC 的副本(Map)

MDC.setContextMap(Map<String, String> contextMap)

将副本设置到当前线程

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

六,日志 {} 占位符和 日志内容拼接性能

我们来通过理论分析 + 实际代码案例 + 性能测试数据,彻底搞清楚这两种写法的性能差异。


📌 核心结论(先看结果)

log.info("message: {}", message);

优秀

推荐在所有场景使用

log.info("message: " + message);

绝对避免,尤其是在非 INFO/ERROR 级别日志中

性能差距:在日志级别关闭时(如 DEBUG 级别日志在 INFO 模式下),第一种写法性能是第二种的 10~100 倍甚至更高


🧠 一、理论分析:为什么 {} 占位符更快?

log.info("message: " + message);

  • 字符串拼接总被执行:无论日志级别是否开启,"message: " + message 这个表达式都会被计算。
  • 创建临时对象:会创建一个新的 String 对象,造成 GC 压力。
  • 无条件开销:即使这条日志最终不会被输出,性能开销已经产生。

log.info("message: {}", message);

  • 延迟计算:SLF4J 会先检查日志级别。如果级别关闭(如 DEBUGINFO 模式下),参数 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 级别日志,大量的 DEBUGTRACE 日志会被过滤。
  • 如果你在 DEBUG 日志中使用字符串拼接,即使这些日志不输出,也会造成巨大的性能浪费(如上例的 1000ms)。
  • {} 占位符是免费的性能优化 —— 它让你的代码在日志关闭时“零开销”。

所以,请永远使用 {} 占位符!
永远不要在日志中使用 + 拼接!

http://www.dtcms.com/a/428015.html

相关文章:

  • 外贸网站推广中山网站手机模板源码下载
  • 网站后台管理模板免费下载网站建设 人性的弱点
  • nodejs动态创建sql server表
  • 做平面设计什么素材网站好使张家港网站建设优化
  • Java 进阶--函数式编程
  • 《道德经》第九章
  • 网站首页怎么做ps中国营销传播网
  • 镇江网友之家百度上如何做优化网站
  • 网站分辨率自适应代码模板网站定制网站
  • 建设网站的网站安全建设银行网站怎么修改手机号码吗
  • 网站后台可以做两个管理系统么wordpress wpenqueuestyle
  • 两种常见的ACM风格笔试题
  • 图神经网络分享系列-transe(Translating Embeddings for Modeling Multi-relational Data) (一)
  • ENVI系列教程(十九)——目标探测与识别
  • 校园超市网站开发整站优seo排名点击
  • 服务器放n个网站自己做鞋子网站
  • Spring核心 - 控制反转 IOC , 用来大量例子来解释
  • 关键词查询的分析网站有网打不开网页咋回事
  • 大型电子商务网站建设成本双流区的规划建设局网站
  • 从引流到生态:排队免单如何重构商家私域流量?
  • 云网站制作的流程图烟台网站制作厂家联系方式
  • 做视频网站要多大的主机中国建设人才信息网查
  • 网站建设发文章几点发比较合适网站开发 平均工资
  • Android dm设备和分区关系以及文件系统介绍
  • 安卓基础组件013--button
  • @MySQL 主从自动修复 - 复制错误
  • 一级造价工程师报名网站枝江市住房和城乡建设局网站
  • 网站上传程序流程it外包公司是什么意思
  • Day1 Linux 入门:9 个核心命令(whoami/id/pwd 等)
  • 【RabbitMQ】docker-compose编排部署RabbitMQ容器——CentOS