
写日志这事在很多 Spring Boot 项目里都是被忽略的一环。刚入行的同学习惯用 System.out.println 输出信息上线后发现问题翻开控制台一看日志早被冲掉了连异常堆栈都找不全。等到项目的确有模有样跑起来、用户量上来之后日志就不再只是“打几行字”那么简单了。所以“springboot 使用 logback 自定义日志”这个题目看起来只是配一个文件实际背后涉及日志框架选型、配置文件加载机制、滚动策略、异步性能、多环境隔离、链路追踪等一系列问题。这篇文章就围绕这个标题把我实际项目里用 Logback 做自定义日志的方案、参数选择、踩坑记录全部拆开讲一遍适合刚接触 Spring Boot 的初学者也适合项目日志还不规范、想补课的团队参考。1. 为什么 Spring Boot 默认选择 Logback以及配置文件的加载机制1.1 SLF4J 门面和 Logback 实现的关系先理清一个很多新人会搞混的概念SLF4J 和 Logback 不是同一个东西。SLF4J 全称是 Simple Logging Facade for Java它本身不干活只定义了一套统一的日志接口。真正的日志输出动作是 Logback 在背后完成的。你可以把 SLF4J 理解成墙上的国标插座Logback 就是符合这个规格的插头二者配合才能通电。项目代码里日常写的都是org.slf4j.Logger和LoggerFactory.getLogger(...)而不是直接 new 一个 Logback 的 Logger 对象。这样写的好处是将来如果团队想换成 Log4j2 或者其他日志实现业务代码一行都不用改只要调整依赖和配置文件就行。Spring Boot 的官方 starter 里已经内置了这种组合spring-boot-starter-logging会自动引入logback-classic同时把 Log4j、JULjava.util.logging通过适配层桥接到 SLF4J保证你项目里即使有老的日志框架依赖输出也能统一走 Logback。所以你在 Spring Boot 项目里做自定义日志默认操作对象就是 Logback不需要额外引入第三方日志依赖。1.2 Spring Boot 对 Logback 的“默认约定”不写任何配置文件时Spring Boot 也会有一套默认的日志行为。这部分默认规则定义在spring-boot包里的base.xml中。默认只输出到控制台格式大概是这样的2025-06-10T14:23:45.12308:00 INFO 12345 --- [http-nio-8080-exec-1] com.example.demo.controller.UserController : 用户查询成功默认级别是 INFO也就是说低于 INFO 的 DEBUG、TRACE 日志不会显示。很多同学初次接触时会觉得这个格式已经够用了但实际项目中你会发现至少三个痛点日志没有落盘重启就丢到处都是控制台输出无法按时段、按级别归档写死在代码里的日志级别没法动态调整。这些痛点就是“自定义日志”要解决的问题。Spring Boot 也提供了一组简单的配置入口比如在application.yml里这样写logging: level: root: info com.example.demo.mapper: debug file: name: logs/app.log这种方式适合快速验证但它的表达能力很有限。你想实现按天滚动、单文件大小限制、异步写盘、不同环境使用不同 pattern这些用 yml 是做不到的。所以正规做法是提供一个独立的 Logback 配置文件完全掌控日志行为。1.3 到底用 logback.xml 还是 logback-spring.xml这是配置自定义日志时第一个要做的选择题。Logback 原生支持logback.xmlSpring Boot 也支持logback-spring.xml二者都可以放在src/main/resources目录下都能被自动加载。区别在于加载时机。logback.xml加载得很早早到 Spring 上下文还没完全初始化。在这个阶段Logback 还不知道application.yml里写了什么因此你无法在logback.xml里读取 Spring 的配置属性也做不到根据spring.profiles.active切换不同的日志策略。而logback-spring.xml是 Spring Boot 专门留的口子它会被 Spring 的LogbackInitializer额外处理支持两个非常关键的标签springProfile和springProperty。前者能根据当前激活的环境加载不同配置块后者可以把 yml 配置文件里的属性值注入到 Logback 配置中。所以我的结论很简单在 Spring Boot 项目里做自定义日志直接用logback-spring.xml别再用logback.xml。名字虽然长一点但灵活度完全不是一个级别。后面所有示例都基于logback-spring.xml。2. 自定义日志的核心配置拆解与实操2.1 三大核心对象Logger、Appender、LayoutLogback 的体系可以拆成三个核心部分Logger、Appender、Layout。Logger 是你在代码里调用log.info()、log.error()时拿到的那个对象。Logger 有层级关系名字叫com.example.demo.controller.UserController的 Logger 会自动继承com.example.demo.controller的配置最终继承到ROOT。这种继承关系让日志级别可以逐层覆盖不需要给每个类单独配置。Appender 决定了日志“写到哪去”。常见的有ConsoleAppender控制台、FileAppender固定文件、RollingFileAppender滚动文件、AsyncAppender异步包装器。一个 Logger 可以绑定多个 Appender比如业务日志既想输出到控制台又想落盘归档就可以挂两个 Appender。Layout 负责格式化日志内容决定每行日志长什么样。早期 Logback 常用PatternLayout现在官方推荐使用 Encoder比如PatternLayoutEncoder因为 Encoder 可以更高效地控制输出字符集和格式。这三者的关系可以用一条流水线来类比Logger 是生产日志的工人Appender 是输送日志的传送带Layout 是决定产品外包装的机器。三者配合才能把日志从代码里送到控制台或磁盘。2.2 一套可以“抄作业”的基础配置模板先给一套我在公司小项目中实际使用的配置模板覆盖了最常见的需求控制台输出、INFO 日志按天归档、ERROR 日志单独归档所有文件限制大小和保留时间。?xml version1.0 encodingUTF-8? configuration !-- 控制台输出 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- INFO 日志滚动文件 -- appender nameFILE_INFO classch.qos.logback.core.rolling.RollingFileAppender filelogs/app.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePatternlogs/app.%d{yyyy-MM-dd}.%i.log/fileNamePattern maxFileSize50MB/maxFileSize maxHistory30/maxHistory totalSizeCap10GB/totalSizeCap /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n/pattern charsetUTF-8/charset /encoder filter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchDENY/onMatch onMismatchACCEPT/onMismatch /filter /appender !-- ERROR 日志滚动文件 -- appender nameFILE_ERROR classch.qos.logback.core.rolling.RollingFileAppender filelogs/error.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePatternlogs/error.%d{yyyy-MM-dd}.%i.log/fileNamePattern maxFileSize50MB/maxFileSize maxHistory30/maxHistory totalSizeCap5GB/totalSizeCap /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n/pattern charsetUTF-8/charset /encoder filter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter /appender root levelINFO appender-ref refCONSOLE/ appender-ref refFILE_INFO/ appender-ref refFILE_ERROR/ /root /configuration这段配置里有几个细节值得说明。第一个细节是RollingFileAppender的滚动策略。我选用的是SizeAndTimeBasedRollingPolicy意思是“既能按天滚动也能在单天文件超过 50MB 时继续拆分成.i后缀的文件”。文件名中的%d{yyyy-MM-dd}代表日期%i代表当天拆分的序号。maxHistory30表示最多保留 30 天的日志totalSizeCap10GB表示所有归档日志总大小超过 10GB 时Logback 会自动删除最旧的归档。这两条参数非常重要不然日志文件会无限增长最后把磁盘写满。我见过不止一次因为日志把所有磁盘空间占满导致应用直接假死的事故。第二个细节是 ERROR 日志单独归档。我用了两个LevelFilter把 INFO 和 ERROR 分流。FILE_INFO这个 appender 遇到 ERROR 级别就拒绝相反FILE_ERROR只接受 ERROR。这样排查线上故障时直接打开logs/error.log就能看到所有异常不用在几十万行 INFO 日志里人工翻找。这里有人会问为什么不直接在代码里把 error 打两遍因为 Logger 绑定多个 Appender 时同一条日志会分别进入所有关联的 Appender再用过滤器做分流管理起来比打两遍干净得多。2.3 异步日志的配置与参数选择高并发接口里如果每条日志都同步刷盘IO 会成为性能瓶颈之一。你想想一个接口如果有 10 条日志每条日志都等一次磁盘写入那这些时间累积起来就是用户感知到的延迟。Logback 提供了AsyncAppender它内部维护一个内存队列业务线程只要把日志丢进队列就能立刻返回真正的写盘由后台线程完成。异步配置示例appender nameASYNC_FILE_INFO classch.qos.logback.classic.AsyncAppender queueSize512/queueSize discardingThreshold0/discardingThreshold neverBlocktrue/neverBlock includeCallerDatafalse/includeCallerData appender-ref refFILE_INFO/ /appender然后把 root 里的FILE_INFO换成ASYNC_FILE_INFOFILE_ERROR同理也可以包一层。这几个参数每个都有讲究。queueSize512控制队列长度。队列太短并发一高就大量丢弃太长内存占用会上升。discardingThreshold是丢弃阈值默认值是queueSize的 20%。这是什么意思举个例子队列容量 512当队列剩余空间不足 20% 时Logback 会抛弃级别较低的日志TRACE、DEBUG、INFO只保留 WARN、ERROR。它的目的是防止队列被普通日志塞满、ERROR 进不去。但问题是你的业务日志很多都是 INFO 级如果把 INFO 丢了排查就没依据了。所以我把discardingThreshold设为 0意思是队列满之前不主动丢日志。neverBlocktrue这句话非常关键。默认情况下队列满时调用方线程会阻塞等待队列腾出位置相当于日志系统反过来拖慢了业务线程。设置成 true 之后队列满时日志直接丢弃但保证业务线程不被拖死。生产环境里我建议加上毕竟日志丢失影响的是可观测性接口卡死影响的是可用性。牺牲一小部分日志完整性换取业务稳定这笔账划算。includeCallerDatafalse是在告诉 Logback 不要抓取调用方信息。抓取类名、方法名、行号这些信息需要遍历调用栈开销很大异步场景下还会影响队列元素的大小。如果 pattern 里确实需要显示日志调用的行号可以用%line占位符但要注意这会增加不小的性能开销。这个异步设计有个非常形象的类比餐厅里后厨炒好菜不会亲自端到每个客人桌前而是放在传菜窗口让传菜员统一送。传菜窗口位置有限如果窗口满了还要硬塞厨师就得停下炒菜等窗口空出来。neverBlocktrue相当于告诉厨师窗口满了就别塞后面那道菜晚点上先保证现在桌子上的菜能出。2.4 多环境差异化管理不同阶段的项目对日志的要求不一样。本地开发时希望日志尽量详细、输出到控制台方便直接调试生产环境希望保留重要信息、归档到文件甚至输出成 JSON 方便日志平台采集。这些差异用springProfile标签在同一个logback-spring.xml里就能解决。springProfile namedev root levelDEBUG appender-ref refCONSOLE/ /root /springProfile springProfile nameprod root levelINFO appender-ref refASYNC_FILE_INFO/ appender-ref refASYNC_FILE_ERROR/ appender-ref refCONSOLE/ /root /springProfiledev 环境把 root 级别调到 DEBUG这样 Mapper 层面的 SQL、业务中间过程的参数都能打印出来。prod 环境则保持 INFO避免 DEBUG 日志刷爆磁盘。除此之外如果你想在 prod 环境输出 JSON 格式日志给 ELK 采集可以加一个LogstashEncoder的 appenderappender nameJSON_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/app.json/file !-- rollingPolicy 同上 -- encoder classch.qos.logback.classic.encoder.JsonEncoder/ /appender还需要说的是springProfile和springProperty经常搭配使用。比如 yml 里定义log.path: /data/app/logs然后 logback-spring.xml 里用springProperty scopecontext nameLOG_HOME sourcelog.path/把这个变量读进来后续用${LOG_HOME}引用。这样日志路径这种经常变化的配置就能统一收敛到配置文件里而不是写死在 xml 中。3. 让日志真正好用的三个进阶实战3.1 用 MDC 实现请求全链路日志追踪系统并发上来之后你会遇到一个非常头疼的问题N 个请求同时处理日志文件里大家交叉输出你根本不知道哪几行日志属于同一个请求。排查问题时只能靠时间戳和线程名猜测。线程名虽然有一定参考价值但线程池会复用线程同一个线程可能处理多个请求单靠它根本还原不了完整链路。MDCMapped Diagnostic Context就是 Logback 为解决这个问题提供的机制。MDC 本质是一个线程私有的 Map你可以在处理请求的入口处放入一个请求 ID然后 pattern 里加上%X{requestId}同一条请求链路中的所有日志就都会带上这个 ID。实现方式其实很简单。写一个 Filter在请求进来时生成一个 UUID放到 MDC 里请求结束时移除Component public class TraceIdFilter implements Filter { Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { String traceId UUID.randomUUID().toString().replace(-, ); MDC.put(traceId, traceId); try { filterChain.doFilter(servletRequest, servletResponse); } finally { MDC.remove(traceId); } } }对应 pattern 改成pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{traceId}] %logger{36} - %msg%n/pattern这样每行日志里都会有一个 traceId同一个请求的日志天然就能串联起来。注意二点第一必须用finally移除 traceId。如果你忘了移除线程池里的线程会被复用下一个请求就会带上上一个请求的 traceId导致链路追踪串线。第二如果你在业务代码里用了Async或者线程池提交了子线程任务子线程默认是拿不到父线程 MDC 值的。要么在提交任务时手动把 MDC 的 map 传给子线程要么用 Logback 提供的MDC.getCopyOfContextMap()和MDC.setContextMap()这两个方法来传递。3.2 不重启修改日志级别线上环境排查问题时经常会遇到这种情况日志级别配在 INFO某个底层库只在 DEBUG 级别才输出关键参数想看到这些参数就得改配置重启可重启一次有风险。Spring Boot Actuator 提供了动态调整日志级别的接口在保证安全性的前提下非常实用。先在pom.xml引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency然后在application.yml里暴露 loggers 端点management: endpoints: web: exposure: include: loggers启动项目后查看某个包的当前级别GET /actuator/loggers/com.example.demo.service调整级别POST /actuator/loggers/com.example.demo.service Content-Type: application/json {configuredLevel: DEBUG}这个接口在排查问题时作用很大但注意权限控制。生产环境不能把 actuator 所有端点都暴露给公网只暴露必要的端点并且配合鉴权避免被人利用来获取敏感信息。3.3 日志脱敏与自定义输出格式日志里打印用户手机号、身份证号、银行卡号这类敏感信息一旦日志泄露就是安全事故。正规做法是从源头不记录敏感字段但很多老项目改不动只能在输出层做一次脱敏兜底。Logback 提供了自定义转换器的方式来实现脱敏。思路是写一个类继承ch.qos.logback.classic.pattern.MessageConverter覆写convert方法在输出消息前用正则把敏感信息替换掉public class SensitiveDataConverter extends MessageConverter { private static final Pattern PHONE_PATTERN Pattern.compile((1[3-9]\\d{9})); Override public String convert(ILoggingEvent event) { String original event.getFormattedMessage(); if (original null) { return ; } Matcher matcher PHONE_PATTERN.matcher(original); return matcher.replaceAll($1****); } }然后在 pattern 中把%msg替换成%converterconversionRule conversionWordconverter converterClasscom.example.demo.logging.SensitiveDataConverter/ pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %converter%n/pattern这种做法适合作为最后一道防线但它有个明显缺陷日志里如果包含异步调用栈、异常详情这些内容不会经过 MessageConverter 处理。所以脱敏不能当成唯一的隐私保护手段归根结底还是要规范日志打印行为不该打的信息从一开始就不要打。4. 实战排坑清单与经验总结4.1 高频问题速查表做自定义日志过程中我踩过不少坑也帮同事排查过不少这里整理一份高频问题速查表遇到类似问题可以直接对照。现象原因解决方案配置完全不生效日志还是老样子文件名写错或者根本没放在src/main/resources下检查文件名是否为logback-spring.xml确认编译后的 classes 目录里存在该文件自定义了 pattern但控制台没变化同时存在logback.xml和logback-spring.xml前者优先级更高删掉logback.xml只保留logback-spring.xml日志文件路径找不到报 FileNotFoundException相对路径依赖启动目录服务以服务方式启动时工作目录不同于你预期的路径配置log.path为绝对路径比如/data/app/logs并用springProperty注入日志中文乱码encoder 未指定 UTF-8 字符集在encoder里加charsetUTF-8/charsetMyBatis 的 SQL 日志不打印Mapper 接口所在包的日志级别太高把对应 package 的级别设为 DEBUG例如logger namecom.example.demo.mapper levelDEBUG/日志文件单天产出非常大磁盘告警只配了maxFileSize没配maxHistory和totalSizeCap完善滚动策略设置保留天数和总量上限异步模式下队列丢了不少日志discardingThreshold用了默认值低级别日志被主动丢弃设为 0并确认neverBlock为 trueERROR 日志打印了两遍多个 Appender 都包含了 ERROR且没有做好过滤器用LevelFilter做单项分流这些坑看着都挺基础但每一项都真实发生在实际项目中。尤其是配置文件同时存在多个导致不生效的问题我刚接触 Logback 时就被折腾了一个下午。排查这个问题的技巧是启动时看控制台里有没有 Logback 自己输出的状态信息它会明确告诉你加载的是哪个配置文件。除了上面表格里的问题还有一个容易被忽略的细节不要把日志文件路径配置在项目相对路径下。你用 IDE 启动时工作目录是项目根目录路径没问题但部署到服务器上启动脚本可能是在任意一个目录执行相对路径就会跑到奇怪的位置。最终统一方案是把日志路径收敛到application.yml用${LOG_HOME}传入部署时通过环境变量覆盖。4.2 容器与部署环境下的日志策略传统部署方式下日志写到服务器本地文件是约定俗成的做法。但在 Docker 和 Kubernetes 环境下这个思路要换个方向。容器本身是随时可能销毁的如果你把日志写到容器内的本地路径容器一删日志就一起没了。而且现在主流的做法是用采集 Agent比如 Filebeat、Promtail统一收集节点日志到 ES 或者 ClickHouse采集 Agent 通常直接从 stdout 拉取不会一个个进容器读文件。所以在容器化项目里我的建议是控制台输出仍然是主力并且尽量输出成 JSON 格式方便采集端解析。最简单的办法是用LogstashEncoder替代 PatternLayoutEncoderappender nameJSON_CONSOLE classch.qos.logback.core.ConsoleAppender encoder classnet.logstash.logback.encoder.LogstashEncoder/ /appender这样每行日志是一个 JSON 对象包含时间戳、级别、logger、message、traceId 等结构化字段。采集端拿到之后不需要写复杂的正则解析直接用 JSON path 就能提取字段后续做监控告警也方便得多。如果你一定要在容器里保留落盘日志建议把日志路径挂载到卷或共享存储并用独立卷保存。但不管怎么做都要意识到容器是临时资源日志的管理策略必须和容器生命周期解耦。4.3 个人实操经验小结日志这件事配置越早规范化后面排查问题越省事。我见过太多项目上线前没人管日志上线后出问题才翻日志结果发现日志格式乱成一团、文件又大又杂只能临时加配置、重启服务整个排障过程充满痛苦。如果你现在正好在搭一个新的 Spring Boot 项目建议把日志方案当成基础设施的一部分来设计而不是等出了问题再补。具体落到执行层面我总结三条心得第一日志级别不要拍脑袋定要形成团队规范。DEBUG 是给开发人员本地调试用的生产环境原则上不开INFO 记录业务关键节点的执行情况WARN 记录可以继续运行但值得关注的情况比如重试超过次数、缓存未命中ERROR 只记录真正需要人工介入的异常。大家严格遵守这个界限日志才有分析价值。第二ERROR 日志里必须包含业务主键。比如订单支付失败日志里只有一句“支付失败”排查的人根本不知道是哪笔订单。正确写法是把订单号、用户 ID 都带进日志消息或者放到 MDC 里。这样出现问题时业务方告诉你一个订单号你能直接翻日志定位整条链路。第三上线前检查日志相关的磁盘空间和备份策略。用一个很大的磁盘分区专门放日志设置滚动策略的totalSizeCap避免把系统盘写满导致应用崩溃。同时定期验证归档文件能正常打开、内容没有编码问题真正验证过的备份才有意义。5. 最后分享两个调试 Logback 的实用技巧我在实际配置 Logback 的过程中最痛苦的一类问题是“到底加载了哪个配置文件”“为什么我这个标签没生效”。后来发现 Logback 自身有调试模式启动项目时加上 JVM 参数-Dlogback.debugtrue控制台会打印 Logback 内部解析配置文件的状态包括加载路径、每个 appender 的初始化结果、过滤器的注册情况。这个参数在你怀疑配置没加载时非常有用看到输出基本就能定位问题。还有个技巧是关于 pattern 的。%logger{36}里的数字 36 表示 Logger 名称最多显示 36 个字符超长部分会从包名开始缩短。这个参数对控制台对齐效果影响很大文件日志里一般保持默认就行。如果你希望日志里展示更多的调用位置信息可以使用%F和%L分别表示文件名和行号。但要注意这两个占位符在高并发场景下性能开销明显我在生产环境的文件日志里没有启用只在本地开发的控制台模式里保留。回到自定义日志这件事本身Logback 在 Spring Boot 里就像一个可以根据项目需求随意组装的工具箱。刚开始只需要能输出到控制台慢慢你会发现需要落盘需要滚动归档需要异步需要链路追踪需要脱敏需要 JSON 输出给日志平台。每增加一个需求你就在现有配置上加一个 appender、加一个 filter整个过程你可以一步步来。我个人实际体会是第一次配置的时候最好在本地完整跑一遍所有场景然后针对自己的业务特点再做精简因为网上很多模板是通用型的直接拿来用在生产上很容易出现“能跑但不适合”的情况。