1. 异常复现

工具及组件版本

IDE:IDEA 2023.3.2 (Ultimate Edition)

JDK:11

SpringBoot: 2.7.8

报错场景复现

编辑application.yml文件后,SpringBoot项目启动失败,报错java.nio.charset.MalformedInputException

错误信息如下:

org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 2
	at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:218)
	at org.yaml.snakeyaml.reader.StreamReader.ensureEnoughData(StreamReader.java:176)
	at org.yaml.snakeyaml.reader.StreamReader.ensureEnoughData(StreamReader.java:171)
	at org.yaml.snakeyaml.reader.StreamReader.peek(StreamReader.java:126)
	at org.yaml.snakeyaml.scanner.ScannerImpl.scanToNextToken(ScannerImpl.java:1218)
	at org.yaml.snakeyaml.scanner.ScannerImpl.fetchMoreTokens(ScannerImpl.java:329)
	at org.yaml.snakeyaml.scanner.ScannerImpl.checkToken(ScannerImpl.java:251)
	at org.yaml.snakeyaml.parser.ParserImpl$ParseImplicitDocumentStart.produce(ParserImpl.java:214)
	at org.yaml.snakeyaml.parser.ParserImpl.peekEvent(ParserImpl.java:166)
	at org.yaml.snakeyaml.parser.ParserImpl.checkEvent(ParserImpl.java:156)
	at org.yaml.snakeyaml.composer.Composer.checkNode(Composer.java:93)
	at org.yaml.snakeyaml.constructor.BaseConstructor.checkData(BaseConstructor.java:124)
	at org.yaml.snakeyaml.Yaml$1.hasNext(Yaml.java:509)
	at org.springframework.beans.factory.config.YamlProcessor.process(YamlProcessor.java:198)
	at org.springframework.beans.factory.config.YamlProcessor.process(YamlProcessor.java:166)
	at org.springframework.boot.env.OriginTrackedYamlLoader.load(OriginTrackedYamlLoader.java:88)
	at org.springframework.boot.env.YamlPropertySourceLoader.load(YamlPropertySourceLoader.java:50)
	at org.springframework.boot.context.config.StandardConfigDataLoader.load(StandardConfigDataLoader.java:54)
	at org.springframework.boot.context.config.StandardConfigDataLoader.load(StandardConfigDataLoader.java:36)
	at org.springframework.boot.context.config.ConfigDataLoaders.load(ConfigDataLoaders.java:108)
	at org.springframework.boot.context.config.ConfigDataImporter.load(ConfigDataImporter.java:128)
	at org.springframework.boot.context.config.ConfigDataImporter.resolveAndLoad(ConfigDataImporter.java:86)
	at org.springframework.boot.context.config.ConfigDataEnvironmentContributors.withProcessedImports(ConfigDataEnvironmentContributors.java:116)
	at org.springframework.boot.context.config.ConfigDataEnvironment.processWithProfiles(ConfigDataEnvironment.java:311)
	at org.springframework.boot.context.config.ConfigDataEnvironment.processAndApply(ConfigDataEnvironment.java:232)
	at org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor.postProcessEnvironment(ConfigDataEnvironmentPostProcessor.java:102)
	at org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor.postProcessEnvironment(ConfigDataEnvironmentPostProcessor.java:94)
	at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEnvironmentPreparedEvent(EnvironmentPostProcessorApplicationListener.java:102)
	at org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.onApplicationEvent(EnvironmentPostProcessorApplicationListener.java:87)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:131)
	at org.springframework.boot.context.event.EventPublishingRunListener.environmentPrepared(EventPublishingRunListener.java:85)
	at org.springframework.boot.SpringApplicationRunListeners.lambda$environmentPrepared$2(SpringApplicationRunListeners.java:66)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:120)
	at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:114)
	at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:65)
	at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:343)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:301)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292)
	at com.test.monitor.MonitorApplication.main(Application.java:30)
Caused by: java.nio.charset.MalformedInputException: Input length = 2
	at java.base/java.nio.charset.CoderResult.throwException(CoderResult.java:274)
	at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)
	at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.base/java.io.InputStreamReader.read(InputStreamReader.java:185)
	at org.yaml.snakeyaml.reader.UnicodeReader.read(UnicodeReader.java:125)
	at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:183)
	... 43 common frames omitted

配置文件更改内容如下:

spring:
  cloud:
    nacos:
      discovery:
        # 测试服务器地址
        server-addr: 127.0.0.1:8848

2. 问题定位

由报错信息中的的MalformedInputException可知,这里是出现了编码问题,可能的选项有以下几种:

  1. 配置文件的格式有问题;
  2. 文件带了BOM,覆盖掉了默认的配置;
  3. 输入了编码错误的字符串,优先怀疑中文。

逐一排查,发现文件的格式没有问题,也没有BOM,推测是中文注释出了问题。

注释掉中文注释,不再报错。

解决临时问题,接下来需要找到根本原因。

我们继续从异常信息下手,可以看到org.springframework.beans.factory.config.YamlProcessor.process(YamlProcessor.java:198)方法调用,这儿就是yaml文件解析为字符串的方法。该方法源码如下:

	private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
		int count = 0;
		try {
			if (logger.isDebugEnabled()) {
				logger.debug("Loading from YAML: " + resource);
			}
            // 这里完成了比特流到字符流的转换
			try (Reader reader = new UnicodeReader(resource.getInputStream())) {
				for (Object object : yaml.loadAll(reader)) {
					if (object != null && process(asMap(object), callback)) {
						count++;
						if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
							break;
						}
					}
				}
				if (logger.isDebugEnabled()) {
					logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
							" from YAML resource: " + resource);
				}
			}
		}
		catch (IOException ex) {
			handleProcessError(resource, ex);
		}
		return (count > 0);
	}

可以看到代码中第8行完成了比特流到字符流的转换,debug模式下也正是这里出现了错误,通过截取resource.getInputStream()中的内容可知,注释中的中文果然出现了乱码。测试可知乱码的编码格式是gbk

那么为什么在编码格式不正确的时候,只要去掉英文字符就可以正常使用呢?

是因为配置中除了中文字符之外,配置之中的其他字符都属于ASCII码表中的字符,这部分字符在各种编码中都是固定不变的,所以即使是错误的编码也读取出了正确的信息。

注意:这里讲到是常见编码格式,例如UTF-16,UTF-16等因为长度原因是不兼容ASCII格式的。

那么进一步的,为什么IDE没有提示我文件编码信息错误呢?

进一步检查IDEA中的文件编码配置,路径”Settings-->File Encodings“。然后发现“Project Encoding”的编码里写着:<System Default: GBK>, 至此,真相大白。

IDE的项目编码为系统默认的GBK,覆盖了全局编码UTF-8格式,导致输入的字符都为GBK,但是IDE是可以正常识别该编码的。

编码相关知识参考博客 ASCII、Unicode、UTF-8、UTF-16、GBK、GB2312、ANSI等编码方式简析

3. 问题处理

  1. Project Encoding页面统一的编码改成UTF-8
  2. 给文件加上UTF-8格式的BOM也可以解决问题,增加之后再删除BOM可以避免自己更改编码错误的内容
  3. 将系统编码改为UTF-8,参考文档

4. 参考

[1] ASCII、Unicode、UTF-8、UTF-16、GBK、GB2312、ANSI等编码方式简析