升级一时爽,配置火葬场。上周我把一个老项目从 Spring Boot 3.2.x 升到 3.3.4,本来只是例行补丁升级,结果应用刚启动就给我抛了个java.lang.IllegalStateException,直接卡死在日志初始化阶段。看了一眼堆栈,ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy下面一片红,之前跑得好好的回滚策略配置,到了新的 Spring Boot 版本里居然完全不被接受。这篇文章就记一下这次踩坑、排查和修复的完整过程,给同样从旧版本升上来的同学一个参考。排查本身不复杂,但里面涉及的 Logback 版本差异和配置语义变化,值得花几分钟弄清楚。
1. 升级后我先遇到了什么
1.1 事故现场:启动直接抛异常
先说升级动作,其实就改了pom.xml里 Spring Boot 的 parent 版本:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> </parent>mvn clean package一切正常,我以为升级就算结束了。结果java -jar启动时,控制台还没打出应用启动横幅,就先来了一段红色堆栈:
java.lang.IllegalStateException: SizeAndTimeBasedRollingPolicy requires fileNamePattern to contain both %d and %i tokens at ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy.start(SizeAndTimeBasedRollingPolicy.java:101) at ch.qos.logback.core.rolling.RollingFileAppender.start(RollingFileAppender.java:115) ...意思很直白:SizeAndTimeBasedRollingPolicy要求文件名模板里必须同时包含%d和%i两个占位符,缺一个都不行。
我当时的老配置大概长这样:
<appender name="FILE" 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}.log</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> <totalSizeCap>2GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender>当时我下意识觉得这配置没问题,因为它在本地和测试环境都跑了大半年。但报错就指向fileNamePattern:我只写了%d{yyyy-MM-dd},没有%i。在旧版 Logback 里这是被宽容处理的,到了新版却成了硬伤。
再说一个细节:这里的%i不是随意加的占位符,它是“同一时间窗口内的归档文件序号”。只要一个滚动周期内可能产生多个文件,就必须用%i区分,否则会互相覆盖或直接写同一个文件。旧版不强制,新版启动期强制校验,直接帮我把隐藏风险挖了出来。
1.2 对比版本差异:罪魁祸首是 Logback 1.5 的校验收紧
既然报错来自 Logback,我第一时间去查 Spring Boot 3.3.4 默认管理的 Logback 版本。Spring Boot 通过spring-boot-dependencies这个 BOM 统一管理依赖版本,3.3.x 系列用的已经是 Logback 1.5.x,而 3.2.x 系列用的还是 1.4.x。
我本地实际拉到的版本是logback-classic 1.5.8、logback-core 1.5.8,升级前是1.4.14。从 1.4 到 1.5,Logback 在配置校验上做了明显的“严打收紧”:
SizeAndTimeBasedRollingPolicy启动时必须同时校验到%d和%i;- fileNamePattern 里如果只有时间占位符,直接抛
IllegalStateException; - 如果
maxFileSize配了但maxHistory/totalSizeCap没配,有些场景也会出校验问题。
这种 fail-fast 的改动,对长期维护的老项目来说是好事,但对升级者来说就是意外炸弹。毕竟旧版本启动不报错不代表配置是合法,只是 Logback 一直没做强制校验。出现这种兼容性问题,本质上是“旧配置本身不规范 + 新版本不再容忍”。
你也可以自己确认:运行mvn dependency:tree -Dincludes=ch.qos.logback,看输出里有没有两个 1.5.x 的 jar。如果看到 1.4.x 和 1.5.x 混在一起,那说明有其他依赖又覆盖了版本,这种情况比单纯配置不兼容更麻烦。
2. 不兼容的根源:回滚策略的机制差异
2.1 时间回滚和大小回滚到底有什么区别
很多同学把TimeBasedRollingPolicy和SizeAndTimeBasedRollingPolicy混着用,实际上这是两种不同维度的滚动策略。
TimeBasedRollingPolicy只根据时间窗口滚,比如每个小时一个文件、每天一个文件,它只需要fileNamePattern里有%d,同时配合maxHistory控制保留数量。它不需要%i,因为一个时间窗口内理论上只会生成一个归档文件。
SizeAndTimeBasedRollingPolicy是时间+大小双维度触发。比如“每天滚一次,但如果单个日志文件达到 100MB,也提前滚”。这时同一个时间窗口内就可能产生多个文件:app.2025-03-10.0.log、app.2025-03-10.1.log。如果没有%i,第二个滚动文件该叫什么名字?Logback 无法用一个固定时间戳表达“这是今天第几个文件”,所以%i就是那个递增序号。
旧配置的问题就在这里:我用了SizeAndTimeBasedRollingPolicy,却在fileNamePattern里只给了一个时间戳。旧版本可能默认把重复文件名覆盖掉,也可能在运行期才报错,新版本干脆在start()阶段就把这个非法组合拦住了。
2.2 为什么说新版本要求是合理的
从文件系统角度看,app-2025-03-10.log这个名字在一个时间窗口内只能对应一个物理文件。当天第一次触发滚动,当前日志被改名为这个归档文件,同时创建新的app.log继续写;当天第二次触发滚动,又要生成app-2025-03-10.log,这时会发生什么?
- 要么直接覆盖旧的归档文件,日志数据丢失;
- 要么 Logback 认为“已经存在同名文件”,跳过归档,日志永远不滚动。
这两种结果都是生产事故级别的。所以 Logback 1.5 在启动时就把这种配置判死,不让它带病运行。想明白这点后,我对这次升级报错就没那么反感了,反而觉得应该早点报出来。
我后来翻了一下 Logback 官方文档,规范写法很清晰:
<fileNamePattern>log/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>如果还想压缩归档,可以加.gz后缀:
<fileNamePattern>log/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>注意%d和%i之间要用.或-分隔,不要直接黏在一起,否则解析出来的文件名可读性很差。
2.3 回滚策略配置参数速查表
为了后续排查方便,我把三类常用策略的配置参数整理成了表格,全部基于 Spring Boot 3.3.4 + Logback 1.5.x 实测结果:
| 配置项 | 所属策略 | 作用 | 是否必填 | 示例 |
|---|---|---|---|---|
fileNamePattern | 全部滚动策略 | 归档文件命名模板 | 必填 | app.%d{yyyy-MM-dd}.%i.log |
maxFileSize | SizeAndTimeBasedRollingPolicy | 触发滚动的大小阈值 | 按需 | 100MB |
maxHistory | 全部滚动策略 | 保留归档文件的最大天数/周期数 | 按需 | 30 |
totalSizeCap | 全部滚动策略 | 所有归档文件总大小上限 | 按需 | 2GB |
cleanHistoryOnStart | 时间相关策略 | 是否在启动时清理过期归档 | 可选 | true |
maxFileSize单位 | 大小策略 | KB/MB/GB,不支持小写之外的写法 | 必填 | 50MB |
这里有个容易踩的坑:totalSizeCap不是“达到上限就立刻删除旧文件”,它是每轮滚动发生后,检查所有归档文件总大小是否超过阈值,超过则按最旧优先删除。所以如果你一天只有几 MB 日志,突然写了一天 5GB 日志,可能不会像大小限制那样精确控制在totalSizeCap下,会有一定滞后。理解这个机制,就不会在监控图里看到日志目录瞬间超限时骂 Logback。
3. 完整修复方案与实操步骤
3.1 先确认 Logback 版本,排除依赖混乱
修配置之前,我建议先跑两条命令,确认当前项目实际解析到的版本,否则改了配置也可能因为依赖版本混乱继续出幺蛾子。
第一条是 Maven 依赖树:
mvn dependency:tree -Dincludes=ch.qos.logback正常情况下会看到类似输出:
[INFO] +- ch.qos.logback:logback-classic:jar:1.5.8:compile [INFO] +- ch.qos.logback:logback-core:jar:1.5.8:compile如果是 1.4.x,说明 Spring Boot BOM 没生效,可能你在某处显式写了logback.version,或者有别的依赖把它降版本了。遇到这种情况,升级问题会变得非常奇怪,比如报错文案是 1.4 的,行为却像 1.5,因为类加载器里可能混着两个版本的 jar。
第二条是看 Spring Boot 依赖管理里的版本:
mvn help:effective-pom -Dverbose | grep logback如果输出 1.5.8 或 1.5.9,说明 3.3.4 的版本管理正常。确认版本没问题后,再去动logback-spring.xml。
3.2 改造 logback-spring.xml 回滚策略
我的修复方案很简单:保留双维度滚动策略,把fileNamePattern改成标准写法,同时把maxFileSize、maxHistory、totalSizeCap全部保留。完整配置如下:
<configuration scan="true" scanPeriod="60 seconds"> <springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="logs"/> <appender name="FILE" 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>30</maxHistory> <totalSizeCap>2GB</totalSizeCap> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <root level="INFO"> <appender-ref ref="FILE"/> </root> </configuration>关键点有几个:
%d和%i都用上,这是新版 Logback 的最低要求;%d放前面,%i放后面,可读性好,也符合官方示例;cleanHistoryOnStart顺手加上,可以减少重启后启动日志里出现“日期中最大时间戳”这类干扰;<file>标签只写固定名称app.log,归档名称完全交给fileNamePattern控制。
有一点要提醒:如果你之前配置里没有<file>,只靠fileNamePattern里的日期来写“当前文件”,那升级后一样会踩坑。RollingFileAppender的当前文件应该是一个固定名,归档文件才走fileNamePattern。这也是很多老配置“升级前正常、升级后报错”的常见原因之一。
3.3 验证是否真的按天和按大小滚动
改完配置后,我做了三轮验证,确认滚动策略真的生效,而不只是启动不报错。
先正常启动应用,写一点日志,确认logs/app.log在持续增长。然后临时把maxFileSize改小到 1KB,方便触发大小滚动:
<maxFileSize>1KB</maxFileSize>重启后让应用输出几行日志,再看logs/目录:
app.log app.2025-03-10.0.log app.2025-03-10.1.log app.2025-03-10.2.log看到.0、.1、.2这种递增序号,说明大小维度触发了。再检查归档文件里的内容时间戳,确认它们都在同一天内被切割出来,说明时间窗口没有乱。
确认没问题后,把maxFileSize调回 100MB。接着验证totalSizeCap:可以临时把totalSizeCap调成1KB或5KB,多打几次日志,再执行:
du -sh logs/观察目录总大小是否被限制在阈值附近。实测下来 Logback 不是严格卡在阈值线,而是删到刚好低于totalSizeCap,中间可能有少量多个归档文件的总和略超,但不会无限膨胀。生产环境建议把totalSizeCap留 20% 余量,避免告警误报。
3.4 不写 XML,直接改 application.yml 的滚轮策略
如果你不想维护自定义 XML,Spring Boot 3.3.4 也允许在application.yml里直接配置日志回滚策略,对于简单项目够用。示例:
logging: file: name: logs/app.log logback: rollingpolicy: file-name-pattern: "logs/app.%d{yyyy-MM-dd}.%i.log" max-file-size: 100MB max-history: 30 total-size-cap: 2GB注意,file-name-pattern里的%i同样不能省,规则和 XML 完全一致。如果你同时存在logback-spring.xml和这段 YAML 配置,自定义 XML 会优先,YAML 里的 rollingpolicy 不会生效。所以两个方案二选一,别都配。
另外logging.file.path在 Spring Boot 3.x 里已经不太好用了,建议用logging.file.name指定完整文件名和路径,然后在 XML 里用<springProperty>读取:
<springProperty scope="context" name="LOG_PATH" source="logging.file.name" defaultValue="logs/app.log"/>这里的source是配置项的完整名字,不是目录名。如果你在application.yml里配了logging.file.path=logs,但在 XML 里用defaultValue="logs"覆盖,就可能导致路径解析错乱。
4. 常见问题与避坑经验
4.1 启动报 “Failed to instantiate RollingFileAppender”
升级后如果遇到这种报错,先别急着怀疑 Appender 本身。常见触发原因是maxFileSize或totalSizeCap的“数字 + 单位”写得不规范。Logback 只认KB、MB、GB这种单位,比如:
100MB合法;100 MB中间有空格,合法,但不够规范;100M有的版本认识,但别赌;100没单位,基本都会报错。
另一个低级错误是单位写成小写mb,新版本对大小写更敏感。统一用大写最稳。
4.2 logback.xml、logback-spring.xml、application.yml 到底谁生效
很多项目里同时存在这三个配置文件,升级后容易互相干扰。Spring Boot 的日志初始化查找顺序是这样的:
logback-test.xml(测试环境)logback-spring.xml(推荐,支持 Spring 扩展)logback.xml(原生 Logback 配置)
如果logback-test.xml和logback-spring.xml同时存在,测试环境用前者,生产环境用后者。如果只有logback.xml而没有logback-spring.xml,那么<springProperty>、<springProfile>这些标签不会被解析,你会看到日志路径变成UNDEFINED或直接抛XML_JANITOR相关错误。
我的建议:清理掉项目里所有logback.xml,只保留一个logback-spring.xml,放在src/main/resources下。然后所有滚动策略只在这一个文件里维护,避免和application.yml的日志配置冲突。
4.3 升级后 totalSizeCap 不生效怎么办
如果启动没报错,但日志目录无限膨胀,先检查你是否用对了策略。totalSizeCap在TimeBasedRollingPolicy和SizeAndTimeBasedRollingPolicy里都支持,但有一个前提:必须有归档文件产生,才会触发清理。如果你发现应用一直写同一个app.log,永远不归档,那totalSizeCap就形同虚设。
这时候要排查两个点:
- 当前 active 文件
app.log是否被反复写入而没有切割; fileNamePattern是否包含%d,不包含则时间滚动永远不工作。
我曾经遇到一个案例,同事把fileNamePattern写成了${LOG_PATH}/app.log.${date},里面用的是${date}而不是%d,旧版 Logback 没有解析,导致它把${date}当字面量拼进文件名,所有归档都生成一个名为app.log.${date}的文件,互相覆盖。升级到 1.5 后启动直接报错,这才暴露了问题。
4.4 不要随便自定义 RollingPolicy 子类
网上有些老项目为了裁剪日志,会自己写一个类继承SizeAndTimeBasedRollingPolicy,然后重写某些方法。升级后这类代码很容易出NoSuchMethodError或NoClassDefFoundError,因为 Logback 1.5 的内部 API 有调整,不是简单地换个版本就兼容。
我自己吃过几次亏后,现在的原则是:能用内置策略就用内置策略,能不改源码就不改。实在有特殊需求,优先用 Filter、Layout 或者自定义 TriggeringPolicy 这类扩展点,少动 RollingPolicy 的继承链。
Logback 1.5 里一个比较常见的变化是start()方法的启动顺序更严格了,子类一旦没按规范调用super.start(),就会在组件初始化时报空指针,而且堆栈指向的往往不是你自己的类,排查起来特别浪费时间。
4.5 升级时顺手检查一下日志 Pattern
除了滚动策略,Spring Boot 3.3.4 升级到 Logback 1.5 后,部分通配符和转义规则也有细微差异。比如默认控制台模式里的%clr彩色日志,在自定义 encoder 里如果没引入对应转换器,可能出现部分日志颜色丢失,但不影响功能。
更需要注意的是%replace、%ex这类转义规则,如果你在 pattern 里用了特殊字符,1.5 对未闭合括号的报错信息更明确,同时也更严格。我给的建议是:升级时把所有日志 pattern 里的%msg%n统一规范,后面加%n不要省略,很多诡异的日志拼接问题就是这样消掉的。
最后提醒一个被很多人忽略的检查点:logback-spring.xml里的<springProperty>属性名,升级后如果读不到值,请确认application.yml里的配置路径是否大写、是否带logging.前缀。Spring Boot 3.3 对配置文件 key 的归一化处理有调整,原来的LOG_PATH变量可能拿不到,导致文件被创建成名为UNDEFINED的目录。这属于“看起来是滚动策略问题、其实是属性绑定问题”的典型情况。
这次修复只改了两行配置,但排查过程花了我大半天。个人体会是:Spring Boot 每次大版本升级背后都有一堆依赖语义变化,别只盯着 release notes 里的新特性,日志配置这类基建最容易在升级时炸。如果你也被同样的报错卡住,先看fileNamePattern是否符合“时间占位符 + 序号占位符”的标准结构,基本能解决一半问题。另一个小技巧:改完配置后把maxFileSize临时调到 1KB 或 10KB 压测一下,确认真的会触发滚动,再调回生产值,这比直接上生产观察靠谱得多。