1. 異常復(fù)現(xiàn)

工具及組件版本

IDE:IDEA 2023.3.2 (Ultimate Edition)

JDK:11

SpringBoot: 2.7.8

報(bào)錯(cuò)場(chǎng)景復(fù)現(xiàn)

編輯application.yml文件后,SpringBoot項(xiàng)目啟動(dòng)失敗,報(bào)錯(cuò)java.nio.charset.MalformedInputException

錯(cuò)誤信息如下:

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

配置文件更改內(nèi)容如下:

spring:
  cloud:
    nacos:
      discovery:
        # 測(cè)試服務(wù)器地址
        server-addr: 127.0.0.1:8848

2. 問(wèn)題定位

由報(bào)錯(cuò)信息中的的MalformedInputException可知,這里是出現(xiàn)了編碼問(wèn)題,可能的選項(xiàng)有以下幾種:

  1. 配置文件的格式有問(wèn)題;
  2. 文件帶了BOM,覆蓋掉了默認(rèn)的配置;
  3. 輸入了編碼錯(cuò)誤的字符串,優(yōu)先懷疑中文。

逐一排查,發(fā)現(xiàn)文件的格式?jīng)]有問(wèn)題,也沒(méi)有BOM,推測(cè)是中文注釋出了問(wèn)題。

注釋掉中文注釋?zhuān)辉賵?bào)錯(cuò)。

解決臨時(shí)問(wèn)題,接下來(lái)需要找到根本原因。

我們繼續(xù)從異常信息下手,可以看到org.springframework.beans.factory.config.YamlProcessor.process(YamlProcessor.java:198)方法調(diào)用,這兒就是yaml文件解析為字符串的方法。該方法源碼如下:

	private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
		int count = 0;
		try {
			if (logger.isDebugEnabled()) {
				logger.debug("Loading from YAML: " + resource);
			}
            // 這里完成了比特流到字符流的轉(zhuǎn)換
			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行完成了比特流到字符流的轉(zhuǎn)換,debug模式下也正是這里出現(xiàn)了錯(cuò)誤,通過(guò)截取resource.getInputStream()中的內(nèi)容可知,注釋中的中文果然出現(xiàn)了亂碼。測(cè)試可知亂碼的編碼格式是gbk

那么為什么在編碼格式不正確的時(shí)候,只要去掉英文字符就可以正常使用呢?

是因?yàn)榕渲弥谐酥形淖址猓?strong>配置之中的其他字符都屬于ASCII碼表中的字符,這部分字符在各種編碼中都是固定不變的,所以即使是錯(cuò)誤的編碼也讀取出了正確的信息。

注意:這里講到是常見(jiàn)編碼格式,例如UTF-16,UTF-16等因?yàn)殚L(zhǎng)度原因是不兼容ASCII格式的。

那么進(jìn)一步的,為什么IDE沒(méi)有提示我文件編碼信息錯(cuò)誤呢?

進(jìn)一步檢查IDEA中的文件編碼配置,路徑”Settings-->File Encodings“。然后發(fā)現(xiàn)“Project Encoding”的編碼里寫(xiě)著:<System Default: GBK>, 至此,真相大白。

IDE的項(xiàng)目編碼為系統(tǒng)默認(rèn)的GBK,覆蓋了全局編碼UTF-8格式,導(dǎo)致輸入的字符都為GBK,但是IDE是可以正常識(shí)別該編碼的。

編碼相關(guān)知識(shí)參考博客 ASCII、Unicode、UTF-8、UTF-16、GBK、GB2312、ANSI等編碼方式簡(jiǎn)析

3. 問(wèn)題處理

  1. Project Encoding頁(yè)面統(tǒng)一的編碼改成UTF-8
  2. 給文件加上UTF-8格式的BOM也可以解決問(wèn)題,增加之后再刪除BOM可以避免自己更改編碼錯(cuò)誤的內(nèi)容
  3. 將系統(tǒng)編碼改為UTF-8,參考文檔

4. 參考

[1] ASCII、Unicode、UTF-8、UTF-16、GBK、GB2312、ANSI等編碼方式簡(jiǎn)析