ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入理解SLF4J:从日志门面到Spring Boot实战排查

深入理解SLF4J:从日志门面到Spring Boot实战排查 Spring Boot 项目里的日志满天飞但真要问一句「SLF4J 到底是什么、日志从 logger.info() 到文件里走了多远」很多写了几年代码的人都说不清楚。面试时它又是高频考点实际开发中那些「日志打两遍」「日志框架冲突」「异步日志丢数据」的坑根源也都在 SLF4J 这条链路上。这篇文章从实际项目视角出发把这套机制掰开揉碎讲清楚附带可直接抄走的配置和排查方法。无论你是刚接触 Spring Boot 的新人还是被日志问题折磨过的老手应该都能从中找到需要的东西。1. 先搞清楚SLF4J 在 Spring Boot 里到底扮演什么角色1.1 门面模式日志世界的「USB 接口」SLF4J 全称 Simple Logging Facade for Java翻译过来就是「Java 日志门面」。门面这个词你可以理解成手机充电口的 USB-C 标准手机生产商不需要关心你用哪个牌子的充电头只要充电头支持 USB-C插上就能用。SLF4J 定义了一套统一的日志调用接口你的业务代码永远只对着这套接口写至于背后是 Logback、Log4j2 还是 JDK 自带的 JUL业务代码完全不关心。为什么会出现这种设计因为 Java 日志框架的历史实在太分裂了。早年 Log4j、JUL、commons-logging 各玩各的Spring 内部用 JCLHibernate 用 JBoss LoggingMyBatis 用 Log4j 或 stdout……你引入一个第三方库它可能拖着一套自己的日志实现最后控制台里的日志格式五花八门统一排查问题都困难。SLF4J 的出现本质上是给这个混乱局面定了一个公共标准所有代码都面向 SLF4J 的 API 打日志具体输出交给运行时的某个实现去完成。这个「运行时选择实现」的机制就是 SLF4J 和普通接口封装最大的区别。它不是通过 Spring 依赖注入来切换实现的而是在类加载阶段通过 classpath 下存在哪个 binding 依赖来自动绑定底层的日志框架。这意味着你换日志实现不需要改一行业务代码只需要改 Maven 依赖。1.2 Spring Boot 默认全家桶SLF4J LogbackSpring Boot 官方对日志的默认选型是 SLF4J Logback这个组合不是随便定的背后有很深的历史渊源——Logback 的作者 Ceki Gülcü 同时也是 Log4j 的创始人后来他又主导设计了 SLF4J。所以 Logback 对 SLF4J 的支持是最原生、最彻底的两者在 API 设计上几乎就是配套的。当你创建一个 Spring Boot Web 项目时spring-boot-starter-web 会传递引入 spring-boot-starter-logging里面包含三个核心件logback-classicLogback 的核心实现同时充当 SLF4J 的 binding也就是把 SLF4J 接口调用转接到 Logback 的 Logger 上。logback-coreLogback 底层基础库提供 Appender、Layout、配置加载等能力。log4j-over-slf4j这是一个桥接器后面的章节会专门讲它的作用是把老项目里直接使用 Log4j 的代码调用全部劫持到 SLF4J 这条链路上。所以一个 Spring Boot 项目里你什么都不用配置LoggerFactory.getLogger()拿到的对象底层就已经是 Logback 的 Logger 了。有一点容易忽略Spring Boot 3.x 升级到了 SLF4J 2.xSLF4J 2.x 的绑定机制从原来的StaticLoggerBinder改成了 SPIServiceLoader机制LoggerFactory 会自动扫描 classpath 下的slf4j-provider实现。但核心思想没变仍然是通过 classpath 下是否存在某个 binding 来决定具体日志实现。你只需要知道这个区别就行绝大多数情况下不需要手动干预。1.3 为什么 Spring Boot 死磕 SLF4J而不是直接用自己的接口Spring Boot 自己完全有能力封装一套日志抽象但它没有这么做而是选择了 SLF4J原因值得说道说道。第一是生态问题。Java 开源社区里Spring、Hibernate、MyBatis、Netty 这些头部框架最终都选择了 SLF4J 作为对外的日志门面。Spring Boot 如果自己搞一套等于把整个生态重新撕裂一遍开发者整合第三方库时又要做一层适配完全没必要。第二是绑定机制的优势。SLF4J 在编译期只依赖slf4j-api这个 jar 包体积很小没有任何第三方依赖。你在代码里import org.slf4j.Logger和import org.slf4j.LoggerFactory编译环境里只需要这两个类就够了。真正绑定哪个实现是部署时期由 classpath 决定的。这套机制让「代码与实现完全解耦」不再是口号而是真正落地了。第三也是很多面试官喜欢问的一点为什么不用 commons-loggingJCLJCL 是 Apache 的老牌日志门面但它自己的类加载机制在 OSGi、某些自定义 ClassLoader 环境下会出问题经常出现NoClassDefFoundError或者绑定到错误的实现。SLF4J 的绑定方式简单粗暴——classpath 里有什么就用什么多个绑定就告警。虽然简单但反过来它把冲突问题暴露得更直接反而更好排查。2. 日志从代码到文件的完整旅程绑定与桥接机制2.1 三种关键依赖API、Binding、BridgeSLF4J 体系里有三组完全不同的依赖它们的角色很容易搞混我把它们梳理成一张表类型典型依赖作用放 classpath 的结果APIslf4j-api定义 Logger、LoggerFactory 等接口只有接口没有实现打日志会提示 no providersBinding绑定logback-classic、log4j-slf4j2-impl、slf4j-jdk14把 SLF4J 接口调用转接到具体日志框架决定日志最终输出到哪套框架Bridge桥接jcl-over-slf4j、log4j-over-slf4j、jul-to-slf4j把其他日志框架的调用劫持到 SLF4J让第三方框架的日志也统一走 SLF4J也就是说Binding 是「从 SLF4J 向下走」Bridge 是「从其他框架向上收」。方向正好相反这也是很多人出错的地方——把slf4j-log4j12当成桥接器来用其实它是一个 Binding它的工作是让 SLF4J 的接口调用落到 Log4j 1.x 上。2.2 一条日志请求的完整调用链我拿 Spring Boot 项目里最常见的一段代码来说private static final Logger log LoggerFactory.getLogger(OrderService.class); public void createOrder(String orderId, BigDecimal amount) { log.info(订单 {} 创建成功金额 {}, orderId, amount); }从你调用log.info()开始到日志真正出现在控制台或文件里中间大致经历了这么几步LoggerFactory.getLogger()扫描 classpath找到 Logback 的 binding 实现返回一个ch.qos.logback.classic.Logger实例。你的代码调用log.info(订单 {} 创建成功金额 {}, orderId, amount)这个调用进入 Logback 的 Logger.Logback 的 Logger 先检查当前日志级别是否允许 INFO 输出如果允许才继续。这也是{}占位符能提升性能的根本原因——级别不满足时占位符根本不会被格式化。日志事件被封装成LoggingEvent沿着 Logger 的层级链向上传递com.example-com- root每一级的 effective level 都会参与判断。最终由 Appender 处理。ConsoleAppender 负责输出到控制台RollingFileAppender 负责写文件、按时间或大小滚动。输出之前LayoutEncoder会把日志事件格式化成一行文本——就是你在配置里写的%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n这个 pattern。理解这条链很重要因为排查日志问题时无非就是这个链条上某一个环节出了问题。比如日志不输出先看 Level 是否被过滤日志没有颜色看 ConsoleAppender 是否配置了withJansi日志格式不对看 Encoder 的 pattern 写没写对。2.3 桥接器让别人的日志也归你管你的项目里不可能只有你自己写的代码。Spring Framework 内部用的是 JCL很多老库还在直接调用 Log4j APIJDK 自带的 JUL 也被一些工具类使用。如果不管它们就会出现「主日志走 Logback但 Spring 启动日志和第三方库日志各自乱飞」的割裂局面。SLF4J 的桥接器就是为这个场景准备的jcl-over-slf4j替换 commons-logging把 JCL 调用转给 SLF4J。log4j-over-slf4j替换 Log4j 1.x把直接调用org.apache.log4j.Logger的代码转给 SLF4J。jul-to-slf4j把java.util.logging的调用转给 SLF4J需要在代码里手动调用SLF4JBridgeHandler.install()。这里有一个我在项目中真实踩过的经典大坑log4j-over-slf4j这种桥接器绝对不能和slf4j-log4j12这个 Binding 同时出现在 classpath 里。因为log4j-over-slf4j让 Log4j 的调用走 SLF4J而slf4j-log4j12让 SLF4J 的调用走 Log4j——两个方向一拼日志事件就像两只老鼠互相咬尾巴形成无限循环递归最终把栈打爆。Spring Boot 的spring-boot-starter-logging由于默认引导的是 Logback正常情况下不会引入slf4j-log4j12但如果你在 pom 里手动加过 Log4j 相关的老依赖就必须仔细排掉这个坑。3. Spring Boot 项目里的 SLF4J 配置实操3.1 pom.xml 里到底需要加什么依赖先说结论绝大多数 Spring Boot 项目pom.xml 里你什么都不用加日志依赖已经通过 starter 传递进来了。你可以在 IDEA 的 Maven 窗口里展开spring-boot-starter-web - spring-boot-starter-logging看到的依赖树基本就是 1.2 小节列的那几个包。但有两种情况需要手动调整依赖。第一种是切换日志实现。假设你要用 Log4j2 替换 Logback最稳妥的做法是排除默认 logging starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-log4j2/artifactId /dependency第二种是引入桥接依赖把项目里某个第三方库的旧日志收编。比如你的项目里有历史代码直接使用了org.apache.log4j.Logger那么加这个dependency groupIdorg.slf4j/groupId artifactIdlog4j-over-slf4j/artifactId /dependency注意版本号都不用写因为spring-boot-dependencies这个 BOM 已经帮你管理好了 SLF4J 全家桶的版本。这也是 Spring Boot 配置里最容易忽略的一件事自己手动指定 SLF4J 相关依赖的版本号反而可能覆盖 BOM 管理的版本引入你意料之外的冲突。3.2 logback-spring.xml 核心配置从开发到生产Spring Boot 项目建议使用logback-spring.xml这个名字而不是logback.xml。区别在于logback.xml加载时机太早Spring Boot 还在初始化阶段它无法读取application.properties/application.yml里的配置也无法使用springProfile这类环境切换标签。用logback-spring.xml你才能享受到 Spring Boot 对 Logback 的增强。下面这份配置是我在实际项目里用着顺手的一套基础模板你拿过去改改应用名和日志路径就能用?xml version1.0 encodingUTF-8? configuration !-- 从 application.yml 读取应用名 -- springProperty scopecontext nameappName sourcespring.application.name defaultValueapp/ !-- 通用日志格式 -- property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/ !-- 控制台 Appender -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 滚动文件 Appender -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_PATH:-./logs}/${appName}.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy !-- 按天滚动 -- fileNamePattern${LOG_PATH:-./logs}/${appName}.%d{yyyy-MM-dd}.log/fileNamePattern !-- 保留 30 天 -- maxHistory30/maxHistory /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 开发环境输出到控制台级别 DEBUG -- springProfile namedev root levelDEBUG appender-ref refCONSOLE/ /root /springProfile !-- 生产环境输出到文件级别 INFO控制台按需开 -- springProfile nameprod root levelINFO appender-ref refFILE/ /root /springProfile /configuration几个关键点你要理解springProperty会把 Spring 环境变量注入到 Logback 配置里这样spring.application.name可以直接拼进日志文件名。maxHistory只清理按规则滚动的旧文件不会清理file主文件本身所以磁盘满了的第一现场往往是主文件排查时别忽略。springProfile区分环境比在多个环境写多份配置干净得多。你可以针对某个包路径单独设级别比如让com.example.mapper保持 DEBUG方便本地查 SQL这个按需加logger节点就可以了。3.3 代码里获取 Logger 的正确姿势获取 Logger 有几种方式我的建议一直很明确能用 Lombok 的Slf4j就用Slf4j不能用的类就手写静态字段。Slf4j Service public class OrderService { public void createOrder(String orderId, BigDecimal amount) { log.info(订单 {} 创建成功金额 {}, orderId, amount); } }如果不想引入 Lombok手写的形式是Service public class OrderService { private static final Logger log LoggerFactory.getLogger(OrderService.class); // ... }有几点要特别注意Slf4j生成的字段名是log是private static final的。如果你自己在类里再声明一个log字段Lombok 生成时不会覆盖编译能过但 IDEA 会立刻报警语义也会乱。LoggerFactory.getLogger(OrderService.class)传的类对象决定了%logger{50}在日志里显示的名字。如果你图省事传成接口名或者父类名排查日志时就定位不到具体实现类了。不要把 Logger 声明成 Spring Bean 通过Autowired注入。Logger 不是 Spring 管理的对象这样注入往往拿不到真正的绑定实现而且完全没必要。有些人这么干是因为以为 Logger 有状态需要容器管理其实 Logger 是线程安全的、无状态的静态字段持有完全够用。不要用java.util.logging.Logger或者直接new org.apache.log4j.Logger。在 Spring Boot 里这等于绕开了 SLF4J 统一门面日志格式和级别控制都会脱离你的配置体系。4. 一条日志打两遍SLF4J 冲突排查的完整链路4.1 典型现象与初步判断日志问题里出现频率最高、也最容易让人头疼的就是「同一条日志在控制台打了两遍」或者控制台里出现了一段明显的告警SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/.../logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [jar:file:/.../slf4j-log4j12-1.7.36.jar!/org/slf4j/impl/StaticLoggerBinder.class]这段告警说明 classpath 下同时存在两个以上 SLF4J Binding。SLF4J 遵循「先到先得」原则选第一个绑定作为实现。但问题是另一个绑定对应的日志框架比如 Log4j如果也被某些代码直接调用那这些代码走的还是 Log4j 自己的输出通道。两个通道同时在输出就会出现「同一条业务日志在 Logback 和 Log4j 里各打一遍」的现象。但这里要分清楚日志打两遍不一定都是 SLF4J 多绑定导致的也可能是配置本身的问题。我在排查时一般按这个顺序判断如果两遍日志格式完全一样大概率是重复导入了两个 Appender比如 root 下同时挂了一个配置文件和 springProfile 里的 Appender。如果两遍日志格式不一样一边有颜色、一边没有基本可以锁定是两套日志框架在同时工作也就是 SLF4J 冲突类问题。如果只有部分第三方库的日志乱、自己的业务日志正常多半是桥接器缺失或冲突而不是 Binding 冲突。4.2 Maven 依赖树定位冲突来源一旦确认是多绑定冲突接下来的动作只有一个找到导致冲突的依赖确定是谁把它带进来的。这个过程我基本靠mvn dependency:tree完成。先全量过滤 SLF4J 相关依赖mvn dependency:tree -Dincludesorg.slf4j输出里你会看到类似这样的树形结构[INFO] - org.springframework.boot:spring-boot-starter-web:jar:2.7.0:compile [INFO] | \- org.springframework.boot:spring-boot-starter-logging:jar:2.7.0:compile [INFO] | - ch.qos.logback:logback-classic:jar:1.2.11:compile [INFO] | - org.slf4j:log4j-over-slf4j:jar:1.7.36:compile [INFO] - org.slf4j:slf4j-log4j12:jar:1.7.36:compile看到没有slf4j-log4j12这个 Binding 是被某个依赖带上来的。下一步再用-Dincludes反向追踪它的来源mvn dependency:tree -Dincludesslf4j-log4j12如果 Maven 命令输出太粗IDEA 里的 Maven Helper 插件更好用。装好之后在 pom 文件里切到 Dependency Analyzer 页签搜slf4j-log4j12直接列出所有引入它的传递路径。很多时候罪魁祸首是某个内部组件直接声明了slf4j-log4j12或者某个老旧工具包比如org.apache.zookeeper、org.apache.hadoop相关依赖内部显式依赖了 Log4j 1.x 全家桶。4.3 三种冲突解决方案对比定位到元凶之后解决方案基本有三种按推荐优先级排列第一直接排除多余 Binding。在引入第三方依赖的地方加 exclusion这是最干净的方式dependency groupIdcom.example/groupId artifactIdlegacy-tool/artifactId exclusions exclusion groupIdorg.slf4j/groupId artifactIdslf4j-log4j12/artifactId /exclusion exclusion groupIdlog4j/groupId artifactIdlog4j/artifactId /exclusion /exclusions /dependency第二如果那个第三方库的代码里直接用了org.apache.log4j.Logger那就不能光排除还要引入桥接依赖log4j-over-slf4j把 Log4j 的调用统一劫持到 SLF4J。注意这种情况下千万不能同时留有slf4j-log4j12Binding否则就是第 2.3 节说的无限循环。第三如果项目体量大、冲突依赖多、实在理不清就干脆整体切换日志实现把默认的 Logback 换成 Log4j2按 3.1 小节的排除方式操作然后让所有日志统一走 Log4j2 的配置。Log4j2 的配置管理能力不比 Logback 差而且高并发场景下异步日志性能更优。但切换成本也不小所有日志相关的配置、filter、自定义 Appender 都要迁移一遍只为了解决冲突有点得不偿失我的建议是能修 Binding 就先修 Binding。补充一个判断技巧如果用mvn dependency:tree -Dverbose可以看到 Maven 在处理版本冲突时选择了哪个版本。SLF4J 的 Binding 和 API 版本最好保持一致如果slf4j-api是 1.7.xBinding 却是 2.0.x虽然大多数场景能跑但日志框架本身处于一个「半绑定」状态迟早会出幺蛾子。5. 性能与进阶占位符、MDC 与异步日志5.1 参数化占位符的代价与正确姿势很多人知道logger.info(订单 id 金额 amount)不如logger.info(订单 {} 创建成功金额 {}, id, amount)但真要说清楚为什么又卡壳了。核心在于字符串拼接的时机。普通拼接写法即使当前日志级别是 WARNINFO 日志根本不会被输出但字符串拼接已经在方法调用前完成了——拼接动作白白消耗 CPU还额外创建了几个临时字符串对象。使用{}占位符时SLF4J 内部是延迟格式化的Logger 先判断级别级别不满足就直接返回根本不做任何格式化级别满足时才把参数数组格式化成最终的字符串。这意味着级别过滤掉的不只是输出动作还包括格式化本身的开销。不过占位符不是免费的。在高写入量场景比如每秒几万条日志格式化过程本身也有 CPU 开销。Logback 1.2.x 里{}的参数如果太多比如超过 10 个Jerkson 等格式化器可能会有额外性能损耗。我见过一些团队压测时发现日志成了瓶颈最后靠「减少占位符数量消息对象复用」解决的。普通业务系统其实纠结不到这个层面你只要记住一条循环里别打日志尤其别在循环里打大数据量对象的 toString()。这条比占位符本身的优化重要一个数量级。5.2 MDC用一行配置实现 traceId 全链路追踪MDCMapped Diagnostic Context是 SLF4J 提供的一个线程绑定的 Map它允许你在业务的任何地方往当前线程的上下文里放键值对然后在日志输出 pattern 里直接引用。作用有点像快递包裹上的标签——你贴个条码后续每个中转站都能扫出来。生产环境排查问题没有 traceId 简直寸步难行。最廉价的全链路追踪方案就是 MDC。先定义一个简单的过滤器import org.slf4j.MDC; WebFilter(/*) public class TraceIdFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String traceId UUID.randomUUID().toString().replace(-, ); MDC.put(traceId, traceId); try { chain.doFilter(request, response); } finally { MDC.remove(traceId); } } }然后在 logback-spring.xml 的 pattern 里加一个%X{traceId}property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} - %msg%n/这样每一行日志后面都会带上对应的 traceId从用户请求进入网关开始到最终落库整条链路的所有日志都可以用同一个 traceId 串联起来。你在日志系统里 grep 这个值就能看到一次请求在哪些服务里经过了哪些步骤。MDC 的底层实现是ThreadLocal所以线程池里需要特别注意如果不清理线程复用后 traceId 会串线如果异步线程里需要用原 traceId你得手动把值传过去。我在实际项目里的做法是入口过滤器 put、出口 finally remove同时如果有自定义线程池在提交任务时把父线程的 MDC 内容复制到子线程。这一步最好封装成统一工具别每个线程池都单独写容易漏。5.3 异步 Appender 的配置细节日志写入磁盘是 IO 操作在高并发请求下同步写日志会影响业务线程的响应时间。Logback 的AsyncAppender就是解决这个问题的业务线程只把日志事件丢进一个阻塞队列后台线程从队列里取事件再写入真正的 Appender。一个典型的配置长这样appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender !-- 队列容量默认 256 -- queueSize1024/queueSize !-- 队列剩余容量低于 20% 时直接丢弃 TRACE/DEBUG/INFO 日志保留 WARN/ERROR -- discardingThreshold0/discardingThreshold !-- 队列满时业务线程不阻塞直接丢弃日志 -- neverBlocktrue/neverBlock appender-ref refFILE/ /appender这里的三个参数每一个都有代价queueSize设得越大内存占用越高但能扛住瞬时高峰。discardingThreshold默认是队列大小的 20%意思是当队列快满时INFO 及以下级别会被丢弃保证 ERROR/WARN 不丢。如果你希望一条都不丢就把它设成 0但这会让队列满时出现阻塞配合neverBlockfalse。neverBlock设成 true业务线程永远不阻塞但代价是队列满时日志真的丢。对大多数业务系统来说丢一点 DEBUG/INFO 问题不大但如果你有审计类日志必须百分百落盘那绝对不能开neverBlock甚至不应该用异步。我遇到过一起事故某团队为了日志不丢把discardingThreshold设成 0、neverBlock设成 false结果高峰期队列塞满业务线程全部卡在等待日志落盘上接口 RT 直接飙到 3 秒。事后复盘问题不在异步配置而在日志量本身——循环里疯狂打日志的代码才是根因。所以异步是手段不是银弹先保证业务代码不刷无用日志再谈异步配置。6. 面试高频考点与项目里的几个经验细节6.1 这几道题面试官是真爱问结合我这些年面试候选人和被面试的经验SLF4J 相关的面试题万变不离其宗高频出现的是这几道SLF4J 和 Log4j2、Logback 是什么关系关键答出门面模式和运行时绑定机制别只背概念。为什么 Spring Boot 默认选 SLF4J Logback答出生态统一、Logback 对 SLF4J 的原生支持、BOM 管理版本这三点基本就是完整答案。{}占位符为什么比字符串拼接高效关键点在于字符串拼接在方法调用前就发生了占位符是延迟格式化级别不满足时零开销。SLF4J 的 Binding 和 Bridge 有什么区别能用「方向相反」把这个讲清楚面试官一般会点头。classpath 下有多个 SLF4J Binding 会怎样答出「First binding wins」 告警日志 用 dependency:tree 排查再加一个 solution 案例就是加分项。MDC 的实现原理和注意事项ThreadLocal 线程池串线是必考点最好能主动提出来。6.2 新项目上手时我建议你先做这几件事说得更落地一点。如果你正在起一个新项目或者想把手头项目的日志体系梳理干净我的个人建议按这个优先级推进第一日志依赖层面只信任spring-boot-starter-logging不要手动引入任何slf4j-*的 binding 或 bridge除非你有明确需求。这能避免 90% 的日志依赖冲突。第二拿到项目第一件事就是配logback-spring.xml把统一日志格式、按天滚动、生产 INFO / 开发 DEBUG 这三件事先落地。别等项目上线了再补那时候每个人都会按自己的习惯打得乱七八糟统一成本急剧上升。第三尽早把 MDC 的traceId接进去。我见过很多项目上线半年后想做链路追踪结果发现日志格式里压根没有 traceId几千台机器上百万行日志只能按时间猜那种痛苦真的只有经历过才懂。过滤器加一行 put、finally 里一行 remove、pattern 里加一个%X{traceId}半小时就能搞定它带来的排查效率提升是立竿见影的。最后说一个很细节但影响很大的习惯记错误日志时不要只打消息要把异常对象传进去。// 错误示范异常堆栈丢了只剩一句话 log.error(订单创建失败 e.getMessage()); // 正确示范异常对象作为最后一个参数堆栈才能完整输出 log.error(订单创建失败, e);这个坑我自己的同事踩过不止一次。线上报错时日志里只有一句被截断的提示关键的 Caused by 堆栈完全看不到排查时间成倍增长。SLF4J 里异常堆栈是作为最后一个参数处理的如果你用了{}占位符比如log.error(订单创建失败{}, e.getMessage(), e)都对但最直接的就是把异常对象本身传进去。这一点写进团队规范里能帮你省掉无数个查日志的深夜。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进