DEBUG.WIKI
晚安云;香港 CN2 ¥15/月
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
OnlineSIM:在线接收短信,虚拟号码租用

Spring Boot 4.1.0 一条日志编码失败,为什么下一条 JSON 也坏了

Spring Boot 4.1.0 的结构化日志字节缓冲区在编码失败后可能残留内容,污染同线程下一条事件。4.1.1 已修复,还要处理首次编码错误。

Spring Boot 4.1.0 写结构化日志时,某条事件编码失败,紧接着同一线程上的下一条正常日志也变成无法解析的 JSON。日志中可能出现两个对象拼到同一行,前半段来自已经失败的事件:

The name 'message' has already been written

若随后一行还带着上一条 JSON 的残片,要检查 4.1.0 的字节编码缓冲区,而不只是清理重复字段。这条回归发生在复用线程本地缓冲区的 byte 路径:编码中途抛错后,部分字节未清空,被下一次调用继续使用。

找到第一条编码错误及同线程的后一条事件

常见触发方式是应用附加的键与格式器已有的字段冲突。例如使用 Logstash 格式时,再加入名为 message 的结构化键。下面是应用已有 SLF4J 2 logger 上的诊断片段:

LOG.atInfo()
    .addKeyValue("message", "duplicate")
    .log("event A");
LOG.atInfo().log("event B");

它会故意产生字段冲突,只适合受控环境检查错误路径,不要放入正常生产请求。对应的文件日志配置可以是:

logging.structured.format.file=logstash
logging.file.name=app.jsonl

在故障版本中,A 的错误可能连带破坏 B。要对照原始日志文件、编码错误输出、线程以及两条事件的顺序;只看采集平台的解析失败计数,容易把问题误认为下游换行处理。

字段冲突只是一个触发例子。自定义 WritableJson 在写入时抛出异常、值处理器失败或超出嵌套深度,也可能中断编码。必须先有一次失败写入,不能仅凭“偶尔两条日志连在一起”就认定相同根因。

4.1.1 清理失败写入留下的缓冲区

Spring Boot 4.1.1 已包含正式修正:缓冲区转换过程在 finally 中重置,成功与异常路径都会清理本次使用的状态。采用框架兼容的 4.1.1 或包含同项修复的后续版本,核对实际运行 JAR 和依赖管理结果,不单独复制框架内部类覆盖它。

4.0.x 使用的实现没有这条 4.1.0 缓冲区回归,但回退整个 Boot 版本会改变其他依赖和功能。应优先在项目既定分支内修复,保留依赖与配置差异,回退时同步恢复完整应用产物。

同时修正第一次事件为什么编码失败。把业务键改成不冲突的名字,例如 business_message,并核对 MDC、自定义格式器及值处理器。这个更名会改变日志字段契约,下游索引、告警和查询也要一起检查。

改名能移除这一个触发条件,无法替代缓冲区修复。其他序列化异常仍可能发生;也不要把编码异常吞掉,再以“日志调用没抛到业务层”判断数据完整。

验证下一条独立事件没有受污染

在同一线程上安排一次受控编码失败,再写一条无关的正常事件,检查后者是一条独立、有效的 JSON,字段和文本均属于自己。再验证普通连续日志、中文内容,以及原来的 MDC 和 key/value 字段是否保留。不能只在另一条线程写健康日志,那会绕开线程本地缓存的关联。

toJsonString() 使用单次新建的字符串缓冲区,不属于这条残留字节路径;Logback 的结构化 encoder 和 Log4j2 的字节 layout 则需要关注实际字节输出。若升级后仍有无效 JSON,保留产生它的原始字节与第一条错误,再查自定义编码器、采集边界或其他写入者,不把所有日志异常都归给这个已修复的版本问题。