尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring Boot Nacos绑定 Map 时中文 key 导致启动失败:一次从复现到源码的排查实录

Spring Boot Nacos绑定 Map 时中文 key 导致启动失败:一次从复现到源码的排查实录 Spring Boot Nacos绑定 Map 时中文 key 导致启动失败一次从复现到源码的排查实录版本背景Spring Boot 2.6.5 / Spring Framework 5.3.17 / 配置中心 Nacos现象一句话YAML 里明明写的是嵌套 Map启动却报无法把 Integer 转成 Map换掉 key 里的中文就一切正常。一、问题现象需求场景很常见给按模块区分的一组上限值做配置比如按模块配置单次请求的文件大小上限供各个服务共享。配置长这样file:upload:size-limits:用户中心:100# 模块名 - 上限值(MB)订单中心:200对应的属性类DataComponentConfigurationProperties(prefixfile.upload)publicclassFileUploadProperties{/** 模块名 - 单次上传总大小上限MB */privateMapString,LongsizeLimitsnewHashMap();}启动时直接失败*************************** APPLICATION FAILED TO START *************************** Description: Failed to bind properties under file.upload.size-limits to java.util.Mapjava.lang.String, java.lang.Long: Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.Integer] to type [java.util.Mapjava.lang.String, java.lang.Long] Action: Update your applications configuration报错信息很拧巴目标类型明明是MapString, Long却说要把Integer转成 Map。也就是说Spring 认为file.upload.size-limits这个属性本身的值是一个整数而不是一个嵌套 Map。二、直观排查配置看起来完全没问题按惯例先怀疑自己的配置YAML 语法/缩进用解析器验证过结构正确UTF-8 编码无误多文档 YAML项目里bootstrap.yml是多文档结构---分隔 profile确认file:块在最外层、对当前 profile 生效配置中心冲突检查了 Nacos 里同命名空间下可能加载到的所有 dataIdfile.upload下只有临时目录、分片大小等其它键没有任何一份配置把size-limits写成标量。配置肉眼挑不出毛病但报错是实打实的。这时候就该上手最小化复现。三、转机最小化复现 对照实验不启动整个应用只写一个十几行的独立 Java 程序复用 Spring Boot 自己的YamlPropertySourceLoaderBinderpublicclassRepro{publicstaticvoidmain(String[]args)throwsException{test(中文key,file:\n upload:\n size-limits:\n \用户中心\: 100\n);test(英文key,file:\n upload:\n size-limits:\n user-center: 100\n);}staticvoidtest(Stringlabel,Stringyaml)throwsException{varsourcesnewYamlPropertySourceLoader().load(t,newByteArrayResource(yaml.getBytes()));try{MapString,LongmnewBinder(ConfigurationPropertySources.from(sources)).bind(file.upload.size-limits,Bindable.mapOf(String.class,Long.class)).get();System.out.println(label BIND OK: m);}catch(Exceptione){System.out.println(label FAILED: e.getCause().getMessage());}}}结果中文key FAILED: No converter found capable of converting from type [java.lang.Integer] to type [java.util.Mapjava.lang.String, java.lang.Long] 英文key BIND OK: {user-center100}100% 稳定复现且和 key 的字符强相关。问题锁定在 Spring Boot 的绑定机制上与我的业务配置无关。四、源码层面为什么中文 key 会挂4.1 YAML 拍平Spring Boot 加载 YAML 后会把嵌套结构拍平成扁平的属性键file:upload:size-limits:用户中心:100拍平后变成一条属性file.upload.size-limits.用户中心 100 # 注意值是标量 Integer4.2 宽松绑定的索引机制ConfigurationProperties的绑定走的是Binder→SpringIterableConfigurationPropertySource。这个类为了支持宽松命名大小写、-/_互换等会先构建一份属性名索引Mappings对每个已存储的属性名调用DefaultPropertyMapper.map(String)转成标准化的ConfigurationPropertyName作为索引 key之后按name.isParentOf(...)/containsDescendantOf(...)判断父子关系决定 Map 是逐项合并还是当作单个标量转换。4.3 关键ConfigurationPropertyName.adapt()会静默丢弃非法字符DefaultPropertyMapper.map(String)内部用的是ConfigurationPropertyName.adapt(name, .)——注意是adapt而不是of。两者区别of()严格解析遇到非法字符直接抛InvalidConfigurationPropertyNameExceptionadapt()宽松解析非法字符会被静默丢弃不报错。而在这个解析器眼里非 ASCII 字符中文、全角符号、空格等都不合法。实测Spring Boot 2.6.5输入adapt() 结果说明a.b-ca.b-c正常a.b_ca.b_c正常a.bCa.bC正常a.123a.123正常a.hello worlda.helloworld空格被丢弃a.用户中心a中文段被整体丢弃a.订单-管理a.-中文丢弃-保留4.4 完整因果链把上面几环串起来就全通了YAML 拍平出属性file.upload.size-limits.用户中心 100构建索引时adapt(file.upload.size-limits.用户中心)丢弃中文段解析成了file.upload.size-limits3 段于是索引里file.upload.size-limits这个 key直接指向了这条配置——getConfigurationProperty(file.upload.size-limits)返回了标量100同时containsDescendantOf(file.upload.size-limits)返回ABSENT——因为中文段已丢Spring 看不到任何子元素MapBinder.bindAggregate走到属性存在且无子元素的分支直接执行convert(Integer 100 → MapString, Long)没有 Integer→Map 的转换器 →ConverterNotFoundException→ 启动失败。英文 key 走的是另一条路adapt()正常保留 4 段getConfigurationProperty查不到同名标量属性containsDescendantOf返回PRESENT于是走逐项合并子元素的正常 Map 绑定。一张图总结YAML: size-limits: { 用户中心: 100 } │ YAML 拍平 ▼ 属性: file.upload.size-limits.用户中心 100 │ Mappings 索引构建DefaultPropertyMapper.map → adapt ▼ 中文段被静默丢弃 索引: file.upload.size-limits → 这条配置 │ MapBinder 查询 ▼ getConfigurationProperty 命中标量 100containsDescendantOf ABSENT convert(Integer 100 → MapString, Long) ✗ ConverterNotFoundException五、解决方案方案 A推荐key 用英文/拼音编码另配中文名 → key映射表file:upload:size-limits:# key 只用 ASCIIuser-center:100order-center:200module-name-mappings:# 中文名 → key 的映射-name:用户中心key:user-center-name:订单中心key:order-centerDataComponentConfigurationProperties(prefixfile.upload)publicclassFileUploadProperties{privateMapString,LongsizeLimitsnewHashMap();privateListModuleNameMappingmoduleNameMappingsnewArrayList();/** 按业务模块名可能是中文查上限未配置返回 null */publicLonggetLimitMb(StringmoduleName){if(moduleNamenull){returnnull;}for(ModuleNameMappingmapping:moduleNameMappings){if(moduleName.equals(mapping.getName())){returnsizeLimits.get(mapping.getKey());}}returnsizeLimits.get(moduleName);}DatapublicstaticclassModuleNameMapping{privateStringname;// 业务模块中文名privateStringkey;// 对应 size-limits 的 ASCII key}}两个容易踩的坑映射表不要用MapString, String存中文 key——中文 key 同样会被adapt()丢弃一样挂。所以用List实体存(name, key)对查询时如果前端传的模块名带引号如用户中心记得先replace(\, )。方案 B配置写成 JSON 字符串自定义 setter Jackson 反序列化实测把值直接写成 JSON 字符串然后用Binder绑定在 2.6.5 里依然报同样的错String→Map 也没有转换器。必须自己写 setterfile:upload:size-limits:{用户中心: 100, 订单中心: 200}DataComponentConfigurationProperties(prefixfile.upload)publicclassFileUploadProperties{privateMapString,LongsizeLimitsnewHashMap();/** Spring 把 JSON 字符串塞进来我们用 Jackson 反序列化成 Map */publicvoidsetSizeLimits(Stringjson)throwsJsonProcessingException{this.sizeLimitsnewObjectMapper().readValue(json,newTypeReferenceMapString,Long(){});}}优点配置里保留中文、可读性好缺点多一层手写解析且Data生成的 setter 与你手写的冲突需要调整如用JsonSetter或去掉Data只留必要 getter/setter。方案 C升级 Spring Boot 后验证2.6.5 是 2021 年的版本。较新版本对属性名的字符处理可能有变化建议先写个最小复现跑一下再决定不要盲目升级。升级是重动作一般不单为这一个问题冲版本。六、经验总结YAML 里 Map 的 key 不是随便什么字符串都行。Spring Boot 宽松绑定对 key 字符有限制中文、空格、全角符号等会被ConfigurationPropertyName.adapt()静默丢弃然后报出一个极具迷惑性的转换错误Integer→Map误导人去查值的类型而想不到是key的问题。排查套路最小化复现 → 对照实验中英文 key 各跑一遍→ 读源码。这一步一步下来半小时内就能把配置没错、Nacos 没冲突这类无效排查路线排除掉。读源码的姿势javap -c -l反编译 写探针代码调用关键 APIgetConfigurationProperty、containsDescendantOf、ConfigurationPropertyName.adapt打印中间状态比盯着源码猜快得多。绕不过去就换数据形态绑定层对 key 字符有限制就把中文从 key 挪到List实体的字段值里或者干脆用 JSON 字符串 自定义 setter绕开 Spring 的宽松绑定解析。
返回列表