最近同事开发了一个项目,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一下算了,就打了个异常断点。
image-20230628214623405
重启服务,然后进入了异常断点:
image-20230628214823850
然后翻到上一帧,看到里面有两个字符串局部变量,变量里是文件内容,但是都只有一部分的样子,比如下图,有个变量是停在了xxljob的配置那里。由于对这些代码不理解,我以为是这一行的配置有问题,但是肉眼又看不出来,想着看看网络包算了,反正现在也没啥思路,看看从nacos拿到的是啥。
image-20230628215050350
直接本地wireshark抓包,抓本机和nacos服务器之间的8848端口流量即可
image-20230628215609618
这里可以简单看看,上图,首先是登录nacos,获取到一个token;
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去获取数据,这里会请求好几次,这块没看源码,应该是和配置相关:
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
一开始是请求默认配置,如:
GET /nacos/v1/cs/configs?dataId=test-data-id
接下来是:
GET /nacos/v1/cs/configs?dataId=test-data-id.yaml
再接下来是带profile的:
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
怎么入手分析呢,既然可以本地复现,那就还是用异常端点的方法,端点断住后,从异常栈逐级往上找,看看当前线程是怎么走到这一步的。
image-20230628222629510
进入该nacos方法:
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处,具体加载配置的地方:
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呢,是接收一个字节流的,如下:
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
我们再仔细看看这个异常的解释:
/**
* 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代码有点bug,不应该直接使用-Dfile.encoding编码;没集成的时候,走的是spring boot的代码,具体的,大家自行debug下吧,有点晚了,不写了。