带中文的yaml交给nacos配置中心管理,结果起不来了

问题现象

最近同事开发了一个项目,spring boot技术栈,前期开发一般使用本地配置文件,即application.yml这种,文件里包含中文注释。本地用idea调试,一点问题没有。现在准备集成nacos作为配置中心,所以就把application.yml的内容拷贝到nacos,然后重新启动应用,结果报错了,就是很多人初次使用yaml格式的时候,应该都遇到过,就这么一个问题吧,挡了我一下午:

org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1

image-20230628212857841

image-20230628212916768

定位过程

格式检查

看到这个错误,上网一搜,解决方案很多,基本是说:yml格式没对,或者是删除yml文件中的中文。

我呢,先是找了一堆在线校验yaml格式的网站,把我的文件内容拷进去,都说格式正常。

网站这里也分享两个:

https://onlineyamltools.com/validate-yaml

https://www.yamllint.com/

然后呢,因为以前遇到这个错,也没有仔细分析过,都是肉眼处理,比如直接把中文删了,或者空格弄一弄对齐一下。这次不想这么简单粗暴了,想看看到底他么啥问题,对症下药。

然后,在线网站不是分析了没问题吗,但是问题还在,我想是不是文件里有tab、空白符的混用导致的,想着idea装个yaml插件,功能估计更强,按照下载量排序,装了snakeYaml这个鬼插件,结果idea重启直接失败了,还害我重启了一次电脑,后面才发现插件好几年没更新了,然后果断卸载这鬼插件。

异常端点debug

后面想着还是debug一下算了,就打了个异常断点。

image-20230628214623405

重启服务,然后进入了异常断点:

image-20230628214823850

然后翻到上一帧,看到里面有两个字符串局部变量,变量里是文件内容,但是都只有一部分的样子,比如下图,有个变量是停在了xxljob的配置那里。由于对这些代码不理解,我以为是这一行的配置有问题,但是肉眼又看不出来,想着看看网络包算了,反正现在也没啥思路,看看从nacos拿到的是啥。

image-20230628215050350

wireshark查看网络包

直接本地wireshark抓包,抓本机和nacos服务器之间的8848端口流量即可

image-20230628215609618

这里可以简单看看,上图,首先是登录nacos,获取到一个token;

代码语言:javascript
复制
POST /nacos/v1/auth/users/login?encoding=UTF-8&username=xxx HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Accept-Charset: UTF-8
User-Agent: Java/1.8.0_202
Host: xxx:8848
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 23
password=xxx
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Security-Policy: script-src 'self'
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDQU9LTCIsImV4cCI6MTY4Nzk1OTUyMn0.kGYMSgf_TF6OGHdacZXNSwKM_ir2sHs8RcCXrIA56KQ
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 28 Jun 2023 08:38:42 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{"accessToken":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDQU9LTCIsImV4cCI6MTY4Nzk1OTUyMn0.kGYMSgf_TF6OGHdacZXNSwKM_ir2sHs8RcCXrIA56KQ","tokenTtl":18000,"globalAdmin":false,"username":"xxx"}

后续拿token去获取数据,这里会请求好几次,这块没看源码,应该是和配置相关:

代码语言:javascript
复制
spring:
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        username: xxx
        password: xxx
        enabled: true
        file-extension: yaml #文件扩展名
        server-addr: xxxx:8848
        namespace: 6f51bbf8-e378-4c36-b7c4-xxxxx

一开始是请求默认配置,如:

代码语言:javascript
复制
GET /nacos/v1/cs/configs?dataId=test-data-id

接下来是:

代码语言:javascript
复制
GET /nacos/v1/cs/configs?dataId=test-data-id.yaml

再接下来是带profile的:

代码语言:javascript
复制
GET /nacos/v1/cs/configs?dataId=test-data-id-dev.yaml

我在nacos只配置了dataId=test-data-id-dev.yaml,所以前面两个都是404,只有第三个请求有数据。

在wireshark中查看数据包,发现内容都是对的,UTF8编码的中文,完全可以显示:

image-20230628220417336

然后,十六进制我也仔细看了,没啥问题:

image-20230628220619978

迷茫期

接下来,暂时不知道排查方向了,网上看了会文章,又debug了一会,还是没思路。不过网上文章不少是说编码问题的。

我也就检查了下我的启动命令,结果大吃一惊:

image-20230628220854372

按照我这么多年战斗在编码一线的习惯,一般都不会使用GBK,这里怎么是GBK呢?看了下idea里新项目的默认配置:

image-20230628221233555

因为目前手里的项目确实有比较老旧的,用GBK编码的,不知道是不是项目切换过程中,没注意,就切到新项目也还是GBK了,具体也不记得了。

总之呢,这里的编码会影响到debug启动时的-Dfile.encoding参数,我改成UTF-8后,再启动时,就变成了:

-Dfile.encoding=UTF-8

当然啦,我们除了这么改,也可以自己指定一下:

image-20230628224631615

源码分析问题原因

怎么入手分析呢,既然可以本地复现,那就还是用异常端点的方法,端点断住后,从异常栈逐级往上找,看看当前线程是怎么走到这一步的。

nacos获取配置

image-20230628222629510

进入该nacos方法:

代码语言:javascript
复制
public PropertySource<?> locate(Environment env) {
  nacosConfigProperties.setEnvironment(env);
  ConfigService configService = nacosConfigManager.getConfigService();

long timeout = nacosConfigProperties.getTimeout();
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
timeout);
String name = nacosConfigProperties.getName();
// 1 计算配置key
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}

if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}

CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
// 2 根据key加载配置
loadSharedConfiguration(composite);
loadExtConfiguration(composite);
// 3
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

return composite;
}

接下来,进入3处,具体加载配置的地方:

代码语言:javascript
复制
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
// load directly once by default
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
//1 Loaded with profile, which have a higher priority than the suffix
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}

}

上述代码,看个大概就行(因为我也没仔细看,无从讲起,不过代码看着还行,基本见名知意),然后在1处,会根据profile去获取配置,具体到我们,就是获取dev profile的配置。

image-20230628223149884

接下来,进入下图,总算拿到配置了:

image-20230628223246657

然后,会开始解析这个data,data的解析,是要交给yaml的专门的lib来做的,而lib呢,是接收一个字节流的,如下:

代码语言:javascript
复制
 com.alibaba.cloud.nacos.parser.NacosDataYamlParser#doParse

import org.springframework.beans.factory.config.YamlMapFactoryBean;

protected Map<String, Object> doParse(String data) {
1、
YamlMapFactoryBean yamlFactory = new YamlMapFactoryBean();
2、
yamlFactory.setResources(new ByteArrayResource(data.getBytes()));

Map<String, Object> result = new LinkedHashMap<>();
flattenedMap(result, yamlFactory.getObject(), EMPTY_STRING);
return result;
}

可以看到1处,这个类是spring的yaml解析类,不是nacos的;

2处,就是把data变成字节流(data.getBytes()),然后传给1进行解析。

而问题,恰恰出现在这里,这里的data.getBytes(),会采用平台默认字符集,也就是-Dfile.encoding中指定的字符集,因为我们是指定成了GBK,所以字节流就是GBK格式的。

而后续解析yaml的(在异常断点的上一帧),里面是用的UTF-8格式来解字节流,所以就出错了,就报了文章开头的那个错。

image-20230628224003148

我们再仔细看看这个异常的解释:

代码语言:javascript
复制

/**

  • Checked exception thrown when an input byte sequence is not legal for given
  • charset, or an input character sequence is not a legal sixteen-bit Unicode
  • sequence.
  • @since 1.4
    */

public class MalformedInputException

就是说,这个字节流(GBK)在UTF8中找不到对应的合法的字符,所以就报错了。

为啥没有集成nacos的时候没问题

因为上面出问题的代码是nacos的代码,说白了就是nacos代码有点bug,不应该直接使用-Dfile.encoding编码;没集成的时候,走的是spring boot的代码,具体的,大家自行debug下吧,有点晚了,不写了。