1. 升级踩坑:Spring Boot 3.3.4 一换,Logback 回滚策略先崩了
先说结论:这并不是你写的那段 logback-spring.xml 语法有问题,而是 Spring Boot 3.3.4 默认引入的 Logback 版本出现了一次不大不小的“破坏性升级”。原本在 1.2.x 里用得顺手的TimeBasedRollingPolicy + SizeAndTimeBasedFNATP组合,在新版里直接不被识别,日志不滚动、启动报错、配置静默失效,三种情况我都遇到过。如果你正在做 Spring Boot 升级,且日志文件本来按天加大小回滚,这篇大概率能帮你少折腾半天。
我这次是从 Spring Boot 2.7 一路升到 3.3.4,项目里日志配置原本是这样类的:
<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}.%i.log</fileNamePattern> <maxHistory>30</maxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender>这段配置在 Spring Boot 2.x 时代是标准写法,几乎每个 Java 服务都能见到。启动后每天一个文件,单个文件超过 100MB 会自动拆成.1、.2这种序号,保留最近 30 天。很常规,也没什么难度。
结果升级到 3.3.4 之后,启动日志开始出现类似这样的报错:
java.lang.IllegalStateException: Unrecognized conversion character 'i'看到这个错我第一反应是%i写错了。但检查了一遍,没问题。然后我又怀疑是不是 pom 里显式依赖了老版本 Logback,导致版本冲突。查了一下依赖树,Spring Boot 3.3.4 默认管理的是 Logback 1.5.x,而我项目里确实还残留着 1.2.11 的显式依赖。把显式依赖删掉之后,报错变了:
java.lang.IllegalStateException: failed to initialize RollingFileAppender再往下翻具体异常,才看到关键信息:SizeAndTimeBasedFNATP在 Logback 1.3+ 里被标记为废弃,并且某些场景下直接不兼容。这个时候我才明白,问题的根源不在 Spring Boot,而是 Spring Boot 升级后把 Logback 也带到了新版本,旧配置的“经典组合拳”在 Logback 1.3/1.5 里走不通了。
1.1 升级后常见的三类现象
先别急着改配置,建议你升级前先对照一下有没有下面这些症状,确认是不是同一个病根:
- 第一种:启动报错,
Unrecognized conversion character或者Failed to instantiate,应用直接起不来。这种最好定位,因为异常信息里的类名基本都指向 logback 的 rollingPolicy 相关类。 - 第二种:应用能启动,但日志文件永远只有一个,不滚动、不归档。这种最坑,因为不是报错,而是配置被默认行为覆盖了,你盯着 log 目录半天看不出来问题。
- 第三种:启动后正常,但文件到了设定大小后不是拆分归档,而是直接把旧内容覆盖掉,或者生成的归档文件名是
log.2024-09-01.0.log这种奇怪带0的格式,一看就是%i解析失败导致的。
我这次遇到的是第一种,但群里也有人反馈是第二种和第三种。无论哪种,最后指向的都是同一个原因:Logback 1.2 的旧配置写法,在 1.3+ 版本已经不能继续无脑复制了。
1.2 先确认依赖版本,别被自己的 pom 坑了
动手改配置之前,务必先确认你项目里最终生效的 Logback 到底是哪个版本。Spring Boot 3.3.4 的spring-boot-dependenciesBOM 会统一管理 Logback 版本,默认大概是 1.5.x。但很多老项目会在 pom 里显式声明过logback-classic或logback-core的 1.2.x 版本,导致升级 Spring Boot 后仍然用旧版 Logback,这时候反而不会报错。
如果你没有显式依赖,而是直接被 Spring Boot 带着升到新版本,那么旧配置大概率会炸。建议先执行:
mvn dependency:tree -Dincludes=ch.qos.logback:logback-classic看一下最终解析出来的版本。如果显示 1.5.x,请继续往下看;如果还停留在 1.2.x,说明你自己的显式依赖把 Spring Boot 的版本覆盖了,这时候要么去掉显式依赖,要么顺手升级到 1.5.x,再按新写法调整。
注意:如果你升级 Spring Boot 后日志居然一切正常,未必是配置没问题,很可能是旧 Logback 还在“硬撑”。但这种版本错位的状态很危险,后续扫描漏洞或者其他依赖升级,随时可能把 Logback 拉到新版,那时再炸就不好排查了。
2. 为什么旧回滚策略不兼容:Logback 1.3/1.5 动了哪些底层逻辑
要彻底搞明白,先看 Logback 本身的演进。Logback 1.2.x 是个非常老的稳定分支,很多 Spring Boot 2.x 项目都在用。而 1.3.x / 1.5.x 是后来为适配 Jakarta EE 9+ 而发布的版本,Spring Boot 3.x 正是基于 Jakarta EE 的生态。
按官方说法,1.3 之后的部分内部 API 被清理,一些 deprecated(废弃)的类被移除或降级支持。其中影响最大的就是ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP。
这个类在 1.2.x 里承担着“按时间回滚 + 按大小触发”的角色。它本身不是 RollingPolicy,而是嵌套在TimeBasedRollingPolicy里的TriggeringPolicy。老教程都会告诉你:要按天切分,同时单文件又不想超过固定大小,就用TimeBasedRollingPolicy塞一个SizeAndTimeBasedFNATP。
这套设计在 Logback 1.2.x 里能跑,但属于“组合式”设计。Logback 1.3 之后,官方干脆推出了更直接的SizeAndTimeBasedRollingPolicy。单独一个类,既能按时间,又能按大小,不再需要额外塞TimeBasedRollingPolicy。同时,内部实现也换了处理%i的解析器。如果配置文件里还留着TimeBasedRollingPolicy + SizeAndTimeBasedFNATP,在解析%i时就会拿到不同的上下文,最终导致启动异常或配置不生效。
打个不严谨但容易理解的比方:旧方式像是“时间处理器”和“大小处理器”两个模块对接,中间用一根老式数据线;新版本把这根线的协议改了,两个模块还能共存,但互相之间发不了数据,结果就是处理器直接罢工。
2.1 破坏性变化的本质:不是语法变了,而是类路径和解析器变了
很多初级开发者会以为,XML 标签写错了才会报错。但这次的问题不是标签缺失,而是同一个标签在不同版本里的解析规则不同。
SizeAndTimeBasedFNATP这个类在 Logback 1.2.x 里,会主动解析maxFileSize,然后控制何时触发滚动。而在 Logback 1.3+ 里,这个类虽然还在 jar 包里,但内部标记为 deprecated,并且不再自动从TimeBasedRollingPolicy的配置层级里读取maxFileSize。可能的结果就是:
- 启动时类仍然能加载,但配置项完全不生效;
- 启动时解析
%i直接抛出Unrecognized conversion character; - 万一加载成功,但默认按时间策略走,根本不管大小拆分。
我的理解是,新版本希望开发者直接使用SizeAndTimeBasedRollingPolicy,这样时间模式和大小模式可以同时完成,不再通过“回调”机制间接协作。这也是后续 Logback 演进的方向——减少继承层级,让配置更扁平。
2.2 为什么 Spring Boot 3.3.4 升级会触发这个问题
Spring Boot 的版本升级通常会直接修改spring-boot-dependencies中管理的一批组件版本,Logback 就是其中之一。Spring Boot 2.7 时代管理的是 Logback 1.2.x;Spring Boot 3.x 开始使用 Logback 1.4.x/1.5.x。从 3.2 升到 3.3.4,其实 Logback 主版本没有变,但如果项目是从 2.x 一步到位升 3.3.4,那么就等于 Logback 从 1.2 直接跳到 1.5,跨越了两个大版本,旧配置不兼容几乎必然发生。
所以你在网上搜到的大多数“Spring Boot 3 Logback 配置”示例,都会直接写SizeAndTimeBasedRollingPolicy,很少再看到TimeBasedRollingPolicy + FNATP的组合。如果你手上的资料比较老,照着 2.x 时代的博客改 Spring Boot 3 项目的配置,踩这个坑的概率极高。
3. 实操解决:把旧回滚策略迁移到新配置写法
确认版本和本质原因之后,解决方案很清晰:不玩组合套路,直接换成官方推荐的SizeAndTimeBasedRollingPolicy。这种策略本身就同时支持时间回滚和大小回滚,而且%d、%i都直接解析,不用再额外嵌套。
下面是迁移前和迁移后的对照,建议直接照着改。
3.1 迁移后的新配置示例(Spring Boot 3.3.4 + Logback 1.5.x)
删除旧的TimeBasedRollingPolicy + SizeAndTimeBasedFNATP嵌套,改成下面这样:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> <totalSizeCap>10GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender>文件名字模式里面依然保留%d{yyyy-MM-dd}.%i.log,这是关键。%d负责日期,%i负责当天内的文件序号,两者可以同时存在。如果只写%d,则不会按大小拆分;如果只写%i,则不会按日期归档。SizeAndTimeBasedRollingPolicy的设计本身就默认了两者并存,不需要额外的触发策略。
3.2 参数说明与推荐值
迁移不是把类名换了就行,几个参数必须理解到位,不然配置出来行为不对。
maxFileSize:当前活跃文件达到多大触发滚动。常见写法100MB、1GB。别写成100,缺省单位会有歧义。maxHistory:保留归档文件的天数。这里指保留 30 天,超出部分删除。totalSizeCap:所有日志文件(包括活跃文件和归档文件)总大小达到上限后,删除最旧归档文件。10GB 是我这边的生产标准,你可以按磁盘情况调整。
注意,totalSizeCap在旧版 Logback 里有时候不会被严格执行,尤其是当fileNamePattern里没有%i或者归档频率较低时。新版本中这个参数被处理得比较严格,所以加了之后,要留意日志目录是否会提前清理。
再看一个容易被忽略的细节:<file>的配置要与fileNamePattern的路径保持一个逻辑层级。如果file写logs/app.log,fileNamePattern写logs/app.%d{yyyy-MM-dd}.%i.log,那是正常的,活跃文件叫app.log,归档文件带日期加序号。但如果你把file也写到带日期,会出现同时打开多个文件的问题,甚至造成日志写入混乱。
3.3 想保留旧配置,能不能硬扛?
有人会问:我不想改配置,能不能在 pom 里把 Logback 版本锁回 1.2.x,继续用旧方案?
短期看可以,但非常不建议。Spring Boot 3.x 本身面向 Jakarta EE,如果强制使用 Logback 1.2.x,会出现类库冲突。最典型的问题就是javax.servlet和jakarta.servlet的混用。Logback 1.2.x 依赖旧的 Servlet API,Spring Boot 3.3 运行在 Tomcat 10.1 上,底层的jakarta.*包与 Logback 需要的javax.*包不匹配。你也许能把应用跑起来,但到某些上报错时,根本分不清是不是这个版本错位导致的。
另一个隐患是漏洞扫描。Logback 1.2.x 已经停止安全更新,后续 CVE 修复只在新版本里。你为了日志配置硬锁老版本,等于给生产环境埋雷。
所以我的立场是:配置一次性改到位,别试图反向兼容。Logback 1.5.x 的 API 并不复杂,迁移成本远低于想象。
3.4 多环境配置的注意点
如果你的 logback-spring.xml 有 springProfile 分环境配置,或者多应用共享一套日志配置,迁移时建议保持一致。不要把 dev 环境改成新写法、prod 环境还用旧写法,那样后面排查问题时要分两套思路看,非常痛苦。
我实际工程里是直接把日志配置抽取到公共模块,所有服务共用同一个 logback-spring.xml。这样升级时只要改一次,所有服务统一生效。如果你也准备这么做,建议在公共配置中不要写死<file>的绝对路径,而是用${LOG_PATH:-logs}这种占位符方式,不同服务通过启动参数覆盖。这样日志目录、归档周期、大小阈值都可以由每个服务自己的环境变量控制,但整体策略保持一致。
4. 升级后常见问题与排查技巧
把配置改成新写法之后,很多人还会遇到一些衍生的“小毛病”。这一节我按实际排查顺序整理一下。
4.1 现象:日志文件根本不生成或者不写入
改了配置但logs/app.log始终没出现,或者一直停留在 0 字节。通常不是 logback 配置的问题,而是权限或路径问题。
先看启动日志里有没有Failed to create parent directories之类的警告。如果日志路径是相对路径logs/app.log,在 Spring Boot 应用里通常相对于启动目录,也就是 jar 包所在目录。如果通过 systemd 或 Docker 启动,工作目录可能不是你以为的目录。最简单的办法是加一个绝对路径,或者显式设置LOG_PATH环境变量,确保应用有权限创建目录。
另一个常见坑:logback-spring.xml文件名写错。Spring Boot 默认加载logback-spring.xml,不是logback.xml。如果文件放到了src/main/resources下但名字不匹配,配置不会生效。你可以用logging.config=classpath:logback-spring.xml强制指定,排查时特别管用。
4.2 现象:%i一直为0,每天只生成一个带.0的文件
这个问题在新旧版本都可能出现,但在迁移后更容易暴露。原因通常是配置里fileNamePattern写成了logs/app.%d{yyyy-MM-dd}.%i.log,但没有正确指定maxFileSize,导致大小触发永远不会命中,%i没有机会增长。第一个归档文件%i默认是 0,所以看到.0.log是正常的,关键是后续有没有.1.log、.2.log。
如果文件到了 100MB 也没有生成.1.log,请确认maxFileSize是否写进了SizeAndTimeBasedRollingPolicy内部,而不是写在appender层级。maxFileSize必须作为<rollingPolicy>的子标签,放在外面不生效。
4.3 现象:旧归档文件很快被删除,日志保存天数缩水
配置了maxHistory=30和totalSizeCap=10GB,结果发现七天前日志就没了。这种情况往往不是配置错误,而是totalSizeCap太小,或者单日日志量远大于预期。
maxHistory与totalSizeCap同时存在的时候,遵循“先到先删”原则。如果每天的日志总量超过 1.5GB,七天就会撞到 10GB 上限,系统会删除最老归档文件。想要保留更多天数,就得把totalSizeCap调大,或者减少单文件大小与归档频率。
我在生产环境是这么定的:单文件 200MB,maxHistory30,totalSizeCap50GB。这个组合能扛住中型服务的压力,但又不会让磁盘被无限写满。真实项目还要结合监控告警,不要只依赖totalSizeCap。
4.4 现象:升级后日志格式变了,时间戳或线程信息串格式
有时升级 Spring Boot 3.3.4 后,即使没有改 logback 配置,日志输出格式也和旧版略有差异。这通常不是 logback 配置的问题,而是默认pattern里的输出行为随版本调整。如果你的日志已经被标准化监控收集,比如通过 Filebeat 或 Logstash,任何格式变化都可能导致采集失效。
我建议升级后先跑一个最小案例,人工检查一条 INFO 和一条 ERROR 日志,确认%d、%thread、%logger、%msg等字段的排列和之前一致,再滚动发布。不要盲信“配置没改就不会变”。在很多 Java 基础组件升级中,这类隐式变化很常见。
4.5 现场排查工具:启动时开启 logback 调试
如果配置改了还是不起作用,可以临时打开 Logback 的调试输出,在logback-spring.xml开头加一行:
<configuration debug="true">这样启动时控制台会输出每个 appender 的初始化状态、配置文件路径、解析出来的实际参数。对解决“配置不生效”特别有效。排查完记得去掉,不然第三方依赖也会跟着输出大量调试日志。
还可以用StatusListener打印内部状态,比如:
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />这会把 Logback 启动阶段收集到的所有警告和错误直接打到控制台,比看异常堆栈更直观。
5. 升级前后的进一步思考
5.1 为什么不能只看“应用能起来”就完事
我见过不少同事,升级 Spring Boot 后应用正常启动,就认为一切顺利。但日志滚动这种后台任务,如果不观察几天,根本看不出来问题。尤其是旧配置用了SizeAndTimeBasedFNATP的项目,在 Logback 1.5 里可能应用启动不报错,但实际的滚动策略已经退化为“按天滚动,不按大小拆分”。这种退化不会立即影响业务,但在高并发日志量大时,单个文件会被撑到几个 GB,最终导致磁盘写满或日志采集工具处理失败。
所以升级 Spring Boot 大版本后,日志系统是必查项。我一般会做一个快速验证:
- 启动应用;
- 手动生成一定量日志,把单个文件越过
maxFileSize阈值; - 观察
logs目录是否生成app.log、app.2024-09-01.0.log、app.2024-09-01.1.log; - 修改系统日期(测试环境)或缩短
maxHistory验证旧文件是否被清理。
只有这三个动作都通过,我才敢说日志配置没问题。
5.2 一个值得养成的习惯:把日志配置纳入版本变更清单
升级 Spring Boot 这类框架,容易只关注业务代码、接口变化、数据库驱动兼容性,日志配置常常被忽略。但它的影响范围是全局的,一旦出问题,所有服务的日志都会受影响,排查起来很被动。
我现在的团队有个不成文的规定:任何 Spring Boot 主版本升级,都必须把logback-spring.xml和application.yml的logging部分放进 review 清单。升级前先对照官方迁移文档检查配置项,升级后至少保留 24 小时的现场观察期。把日志问题消灭在灰度阶段,而不是等全量发布后半夜起来看磁盘占用报警。
关于 Logback 迁移,最后再分享一个小技巧:不要把fileNamePattern里的%i放在%d前面。合理的顺序是%d在前、%i在后。旧配置里有些文章会写成app.%i.%d{yyyy-MM-dd}.log,在新版 Logback 里这种顺序会导致归档分组逻辑和意图不符,可能出现每次启动都重命名文件的情况。我踩过这个坑,折腾了一下午才发现是顺序问题。
日志配置本身就是越简单越可靠。能用官方推荐的聚合策略,就别再把两个策略嵌套在一起。这次升级虽然折腾,但也让我把项目里积攒多年的“玄学日志配置”彻底清理了一遍,算是因祸得福。