17. 把 DDD 开源脚手架化为自己的:第四次联调(三)——一坨 MalformedInputException,最后查出来是 yml 里的中文注释

17. 把 DDD 开源脚手架化为自己的:第四次联调(三)------一坨 MalformedInputException,最后查出来是 yml 里的中文注释

上一篇改完 nacos 配置,服务死活起不来,报的是 Failed to configure a DataSource。但真正拦住启动的,是它前面那一大坨没人愿意细看的红色报错栈:org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1。

这一篇就干一件事,把这坨两百行的栈逐帧读懂 。栈不会骗人,它写得很清楚:错在 snakeyaml 解析 nacos 配置那一刻,触发点是 MalformedInputException(字节流按 UTF-8 解码失败),第二次报错的栈里还冒出了 CommentEventsCollector,问题就出在注释上。最后查出来,是 jet-ddd-common.yml 里的中文注释编码不对。

这是第四次联调的第三篇,没写新功能,就是把一个报错栈读到底。别被两百行吓住,关键帧就那么几行。

一、报错来了:一整坨 YAMLException

服务启动,日志里先甩出这么一段。原始栈很长,我只留有信息量的帧,中间 Spring 启动的样板栈折叠掉:

less 复制代码
org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1
        at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:214)
        at org.yaml.snakeyaml.reader.StreamReader.ensureEnoughData(StreamReader.java:171)
        at org.yaml.snakeyaml.reader.StreamReader.ensureEnoughData(StreamReader.java:166)
        at org.yaml.snakeyaml.reader.StreamReader.peek(StreamReader.java:121)
        at org.yaml.snakeyaml.scanner.ScannerImpl.scanToNextToken(ScannerImpl.java:1219)
        at org.yaml.snakeyaml.scanner.ScannerImpl.fetchMoreTokens(ScannerImpl.java:335)
        at org.yaml.snakeyaml.scanner.ScannerImpl.checkToken(ScannerImpl.java:239)
        at org.yaml.snakeyaml.parser.ParserImpl$ParseImplicitDocumentStart.produce(ParserImpl.java:210)
        at org.yaml.snakeyaml.parser.ParserImpl.peekEvent(ParserImpl.java:161)
        at org.yaml.snakeyaml.parser.ParserImpl.checkEvent(ParserImpl.java:152)
        at org.yaml.snakeyaml.composer.Composer.checkNode(Composer.java:119)
        at org.yaml.snakeyaml.constructor.BaseConstructor.checkData(BaseConstructor.java:150)
        at org.yaml.snakeyaml.Yaml$1.hasNext(Yaml.java:525)
        at org.springframework.beans.factory.config.YamlProcessor.process(YamlProcessor.java:203)
        at org.springframework.beans.factory.config.YamlProcessor.process(YamlProcessor.java:169)
        at org.springframework.boot.env.OriginTrackedYamlLoader.load(OriginTrackedYamlLoader.java:85)
        at org.springframework.boot.env.YamlPropertySourceLoader.load(YamlPropertySourceLoader.java:50)
        at com.alibaba.cloud.nacos.parser.NacosDataParserHandler.parseNacosData(NacosDataParserHandler.java:92)
        at com.alibaba.cloud.nacos.configdata.NacosConfigDataLoader.pullConfig(NacosConfigDataLoader.java:147)
        at com.alibaba.cloud.nacos.configdata.NacosConfigDataLoader.doLoad(NacosConfigDataLoader.java:81)
        at com.alibaba.cloud.nacos.configdata.NacosConfigDataLoader.load(NacosConfigDataLoader.java:68)
        ... (中间 ConfigDataEnvironment / ConfigDataImporter / SpringApplication 启动样板栈)
        at vip.wayhua.jet.ddd.rbac.RbacApplication.main(RbacApplication.java:33)
Caused by: java.nio.charset.MalformedInputException: Input length = 1
        at java.base/java.nio.charset.CoderResult.throwException(CoderResult.java:274)
        at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:188)
        at java.base/java.io.InputStreamReader.read(InputStreamReader.java:177)
        at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:118)
        at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:179)
        ... 54 common frames omitted

看三处就够了。异常类型是 YAMLException 套着 MalformedInputException: Input length = 1,这不是 YAML 语法错,是读字节时解码失败,Input length = 1 说的是解码器碰上一个按当前字符集(UTF-8)解不出来的字节。

出错位置在 NacosConfigDataLoader.pullConfig → NacosDataParserHandler.parseNacosData → OriginTrackedYamlLoader,也就是「从 nacos 拉下配置、snakeyaml 解析它」那一步,跟前面 DataSource 那堆业务配置没关系。

真正的根在 Caused by 里:sun.nio.cs.StreamDecoder 一路抛到 CoderResult.throwException 的 MalformedInputException,经 InputStreamReader.read 由 snakeyaml 的 UnicodeReader.read 触发。这条链上没有任何 YAML 语法判断,全是字节转字符的动作。

二、先搞清楚这坨栈为什么会这么长

先补点背景。Spring Boot 3/4 用 ConfigDataEnvironment 统一加载 spring.config.import 里的每一项,optional:nacos:jet-ddd-common.yml 会走 NacosConfigDataLoader,把 nacos 上那份配置拉下来,再交给 YamlPropertySourceLoader → OriginTrackedYamlLoader → snakeyaml 去解析。所以栈从 RbacApplication.main 一路往下,一直压到 snakeyaml.StreamReader.update 才崩。启动、加载 import、拉 nacos、解析 YAML、字节解码,最后那环一失败,整条启动就断在这儿。

再往细看,snakeyaml 自己就分了好几层。最底下是字节层,StreamReader.update → UnicodeReader.read → InputStreamReader.read → StreamDecoder → CoderResult.throwException,读字节、按字符集解码都在这层;往上是 Parser 和 Scanner,ParserImpl 是个状态机,管现在处在哪个语法状态,ScannerImpl 把字符流切成一个个 token,这层才真正在读 YAML 语法;再往上是 Composer 和 Constructor,Composer.checkNode 把事件流拼成节点树,BaseConstructor.checkData 把节点拼成 Java 对象;然后是 Nacos 那层 NacosConfigDataLoader.pullConfig → NacosDataParserHandler.parseNacosData;最上面才是 Spring 的 YamlProcessor.process → OriginTrackedYamlLoader.load → YamlPropertySourceLoader.load,把这份配置解析成 PropertySource。

异常是反着来的:最底下的字节层先崩,然后一层层往上包。所以栈顶写的是 YAMLException,最底下 Caused by 才是 MalformedInputException。看这种栈,先翻到最底下那个 Caused by,别被上面一长串业务名字带跑。

第一坨栈的落点也很具体。Yaml$1.hasNext → BaseConstructor.checkData → ParserImpl$ParseImplicitDocumentStart,说明解析器还停在「文档还没开始」这个状态,连第一个 document 都没开始读;ScannerImpl.scanToNextToken 是扫描器在跳过空白、找下一个 token;StreamReader.peek() 只是往前看一个字符。也就是说,在配置开头那一小段被跳过的区域里(空白或注释),往前看一个字符时,字节就解不动了。

回头再看上一篇那个 Failed to configure a DataSource: url not specified,它只是果。因是这份 common 配置根本没解析成功,jet.mysql.* 这些占位符从没被填进去。

三、第二坨同样的报错,线索落在「注释」上

重试一次,nacos 那边先打了 jet-ddd-common.yml 加载 success,紧接着又是同一段 YAMLException: MalformedInputException。但这坨栈的中间帧不一样了,多了 CommentEventsCollector:

less 复制代码
2026-09-26 19:34:33.367 [main] INFO  c.a.c.n.c.NacosConfigDataLoader - [Nacos Config] Load config[dataId=jet-ddd-common.yml, group=DEFAULT_GROUP] success
2026-09-26 19:34:33.368 [main] ERROR c.a.c.n.c.NacosConfigDataLoader - Error getting properties from nacos: ... dataId='jet-ddd-common.yml' ...
org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1
        at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:214)
        at org.yaml.snakeyaml.reader.StreamReader.ensureEnoughData(StreamReader.java:171)
        at org.yaml.snakeyaml.reader.StreamReader.peek(StreamReader.java:131)
        at org.yaml.snakeyaml.scanner.ScannerImpl.scanPlain(ScannerImpl.java:2059)
        at org.yaml.snakeyaml.scanner.ScannerImpl.fetchPlain(ScannerImpl.java:1089)
        at org.yaml.snakeyaml.scanner.ScannerImpl.fetchMoreTokens(ScannerImpl.java:447)
        at org.yaml.snakeyaml.scanner.ScannerImpl.checkToken(ScannerImpl.java:239)
        at org.yaml.snakeyaml.parser.ParserImpl$ParseBlockMappingKey.produce(ParserImpl.java:637)
        at org.yaml.snakeyaml.parser.ParserImpl.peekEvent(ParserImpl.java:161)
        at org.yaml.snakeyaml.comments.CommentEventsCollector$1.peek(CommentEventsCollector.java:57)
        at org.yaml.snakeyaml.comments.CommentEventsCollector$1.peek(CommentEventsCollector.java:43)
        at org.yaml.snakeyaml.comments.CommentEventsCollector.collectEvents(CommentEventsCollector.java:136)
        at org.yaml.snakeyaml.comments.CommentEventsCollector.collectEvents(CommentEventsCollector.java:116)
        at org.yaml.snakeyaml.composer.Composer.composeScalarNode(Composer.java:249)
        at org.yaml.snakeyaml.composer.Composer.composeNode(Composer.java:214)
        at org.yaml.snakeyaml.composer.Composer.composeValueNode(Composer.java:396)
        at org.yaml.snakeyaml.composer.Composer.composeMappingChildren(Composer.java:361)
        at org.yaml.snakeyaml.composer.Composer.composeMappingNode(Composer.java:329)
        ... (中间 Composer 造节点的多帧,以及 YamlProcessor 加载、SpringApplication 启动样板栈)
        at vip.wayhua.jet.ddd.rbac.RbacApplication.main(RbacApplication.java:33)
Caused by: java.nio.charset.MalformedInputException: Input length = 1
        at java.base/java.nio.charset.CoderResult.throwException(CoderResult.java:274)
        at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:188)
        at java.base/java.io.InputStreamReader.read(InputStreamReader.java:177)
        at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:118)
        at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:179)
        ... 72 common frames omitted

CommentEventsCollector.collectEvents 是 snakeyaml 收集注释事件时走的分支。它能出现在这个栈里,说明 Spring 解析这份 YAML 时把注释收集打开了(OriginTrackedYamlLoader 要靠注释记录配置来源),解析器在给一个标量节点收注释的时候,绕到了扫描器上。

两坨栈摆一起看,差别比表面上多。第一次停在 ParseImplicitDocumentStart,扫描器在 scanToNextToken 里跳过空白;第二次停在 ParseBlockMappingKey,已经在解析 block mapping 的 key,扫描器在 scanPlain 里扫一个普通标量。连 StreamReader.peek 的行号都不一样(121 对 131),第一次的 ensureEnoughData 压了两帧、第二次只有一帧。最大的差别还是那四帧 CommentEventsCollector,第一次根本没有。末尾省略的公共帧数也从 54 变成 72。

第一次是还没进正文就炸,第二次是已经在正文里、而且是在处理注释的过程中炸。两次都崩在 snakeyaml 的字节层,但栈中间那段完全不是一回事。

所以问题不在配置的值上,是配置里的中文注释编码不是合法的 UTF-8。

补充一个容易踩的点:MalformedInputException: Input length = 1 十有八九是 GBK / ANSI 保存的文件被当 UTF-8 读了。中文在 GBK 下是两字节、UTF-8 下是三字节,字节序列对不上,解码器就在中文那一位抛 Input length = 1。nacos 上的配置、本地 yml,只要有一个是用非 UTF-8 存了中文注释,就会在这一步炸。

至于这份带中文注释的 common 配置最终怎么改、改完服务能不能起来,还有起之后冒出来的 rbacSqlSessionFactory 依赖链,放在下一篇收尾。

四、Input length = 1 到底是什么:字节流按 UTF-8 解不动了

把 Caused by 那条链单独拎出来看,它跟 YAML 语法一点关系都没有:

bash 复制代码
Caused by: java.nio.charset.MalformedInputException: Input length = 1
        at java.base/java.nio.charset.CoderResult.throwException(CoderResult.java:274)
        at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:188)
        at java.base/java.io.InputStreamReader.read(InputStreamReader.java:177)
        at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:118)
        at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:179)

从下往上读:StreamReader.update 去 UnicodeReader.read 要字符,UnicodeReader 底下包着 InputStreamReader,按某个字符集解码;StreamDecoder.implRead 解到一半解不动,CoderResult.throwException 把 MalformedInputException 抛出来。整条链全是字节转字符的动作,没有一处在判断 YAML 语法。

Input length = 1 这个数字是解码器给的:我这儿只拿到 1 个字节,按当前字符集凑不出一个完整字符。UTF-8 里一个中文是 3 个字节,如果文件其实是 GBK 存的(中文 2 字节),解到中文那一位,剩下的字节数对不上,解码器只能停在那儿报 Input length = 1。

还有个细节。同一个 StreamReader.update,在这坨栈里出现了两个行号,栈顶是 update(214),Caused by 里是 update(179)。对着 snakeyaml 的源码看,update() 就是读一段字节、转成字符的那个方法,读字节那句在 179,214 是它的 catch (IOException e) { throw new YAMLException(e); }。所以**YAMLException 不是解析器报的语法错,它只是把底下那个 IOException 包了一层**。看到 YAMLException 别急着去查缩进,先看 Caused by 是什么。这也是我上一篇被带偏的原因,一直盯着 DataSource 和占位符,压根没往下翻。

日志里还有一条旁证。第二次报错那行 Error getting properties from nacos: 后面跟了一大串 NacosConfigProperties,里面 encode='null',nacos 客户端这边没指定编码,走的是默认 UTF-8。配置内容只要不是合法 UTF-8,这一步必炸,跟它 YAML 写得对不对没关系。

这坑不是我一个人踩。搜一下这个异常,答案高度一致:中文注释加上文件编码不是 UTF-8,处理办法也就那几种,把文件另存为 UTF-8,或者干脆去掉中文注释 TRAEREF(https://blog.csdn.net/230179501633/article/details/147694275)。还有一篇的栈跟我的几乎同款,'CommentEventsCollector.collectEvents→Composer.composeScalarNode→OriginTrackedYamlLoader TRAE_REF](https://blog.csdn.net/2301_79501633/article/details/147694275)。还有一篇的栈跟我的几乎同款,`CommentEventsCollector.collectEvents → Composer.composeScalarNode → OriginTrackedYamlLoader TRAEREF](https://blog.csdn.net/230179501633/article/details/147694275)。还有一篇的栈跟我的几乎同款,'CommentEventsCollector.collectEvents→Composer.composeScalarNode→OriginTrackedYamlLoaderOriginTrackingConstructor.getData一路对上,它崩在ArrayIndexOutOfBoundsException而不是MalformedInputException`,作者查下来也是读配置注释时出的问题,删掉那行注释就好了 $TRAE_REF。

真要确认编码,有个不费劲的办法:编辑器右下角都写着当前文件编码,VS Code 点一下能「Reopen with Encoding」,换个编码重开一次,中文字段显示成乱码、或者换回来就正常,那基本就坐实了。改法跟收尾一起放下一篇。

五、小结

这一篇没写新功能,就是把一坨吓人的报错栈读明白:

  1. 关键帧就几行。看异常类型(YAMLException: MalformedInputException)、看 Caused by(StreamDecoder / CoderResult 解码失败)、看出现场的调用者(NacosConfigDataLoader.pullConfig → parseNacosData),中间 Spring 启动的样板栈直接折叠。
  2. 栈是分层的,先看最底下的 Caused by。从下往上是字节层(StreamReader / UnicodeReader / InputStreamReader)、Parser 和 Scanner、Composer 和 Constructor、Nacos 取配置、Spring 的 YAML 入口。异常从最底下往上包,栈顶那个 YAMLException 往往只是包装。
  3. 同一坨栈的差异就是线索。第一坨停在文档开始前(ParseImplicitDocumentStart + scanToNextToken),第二坨停在 block mapping 的 key 上(ParseBlockMappingKey + scanPlain),还多了 CommentEventsCollector。从「还没进正文」挪到「注释附近」,坏字节的位置基本就锁定了。
  4. Input length = 1 基本等于编码不匹配。不是 YAML 语法错,是字节流按 UTF-8 解码失败;中文在 GBK/ANSI 和 UTF-8 下字节数不同,用非 UTF-8 存中文,就会在中文那一字节抛出来。nacos 那边 encode='null' 走默认 UTF-8,一样躲不过。
  5. DataSource 报错只是果,配置没解析才是因。上一篇的 url not specified,是这份 common 没被 snakeyaml 解析成功、jet.mysql.* 没填进去导致的。排查要往栈最底层的 Caused by 追,别停在最上面那条业务表象。
  6. 第二次栈里的 CommentEventsCollector 是决定性线索。解析器在给标量节点收集注释,问题就在注释文本上,中文注释编码不是 UTF-8。定位到这儿,改法(把注释改成 UTF-8,或者直接去掉中文注释)就只剩一步。

下一篇,服务起不来还剩最后一道 rbacSqlSessionFactory,以及一个更离谱的发现,project 的库和表根本没建。

相关推荐
智塑未来1 小时前
通过GMP认证的制药MES系统推荐:审计追踪与电子签名能力对比
大数据·人工智能
回眸&啤酒鸭1 小时前
【回眸】私人定制旅游路线助手
人工智能
用户360055579001 小时前
Day 1·3 确定性测试门:推理引擎的PASS/FAIL自检机制
人工智能
知几蜗牛1 小时前
HydraFusion的Single、Cascade与Critique如何落到工程门禁
人工智能
mit6.8241 小时前
plz直接提交 pull equest
人工智能
Solara2 小时前
29 条回复永远没送到:翻完 108 条投递台账,我才发现「微信限流」是我取错的名字
人工智能·agent·ai编程
HelloWorld0012 小时前
告别 Toy Demo!基于 MCP 协议打造生产级 AI 自动化中台:架构演进、权限沙箱与实战踩坑
ai编程
lucas_AI2 小时前
别再无脑堆数据了:腾讯 WeVisDoc 把文档解析卷到 95 分,token 是按预算花的
人工智能
橘和柠2 小时前
一台笔记本上的三国杀:eNSP、VirtualBox 与 Docker,我让它们共存了
人工智能