问题
在项目中时常需要读取配置文件,例如某个变量会随着环境的不同而不同,在springboot项目中会很容易想到yml的方式。但是最近遇到一个问题,发现下划线被注入的到bean中会丢失下划线,差点引起线上故障,demo如下:
@Component
public class YmlConfTest {
@Value("${id}")
private String id;
}
yml配置文件如下:
id : 1_2
实际运行中发现id注入的是12,而不是预期的1_2,这就导致问题发生。
原因分析
习惯性的在网上简单搜索了一下问题,发现很少有描述这个问题的,偶尔几篇说key不能有下划线,但我是value下划线的问题,因此也不是很满意,于是有了这篇文章帮助需要的人,所以本文打算分析为什么会丢失掉下划线,以下非核心逻辑简单描述感兴趣可以自行查阅,重点描述下划线丢失的逻辑。
spring部分
- Spring容器在加载的时候会有事件通知触发相关的监听器Listener逻辑,yml加载就在其中的 ConfigFileApplicationListener
- 监听器经过一系列寻址找到对应的配置文件
- 如果是yml配置文件会找到YamlPropertySourceLoader进行加载配置文件
yml部分
- 最终遍历到配置项的节点,org.yaml.snakeyaml.resolver.Resolver进行key和value的解析任务
public Tag resolve(NodeId kind, String value, boolean implicit) {
if (kind == NodeId.scalar && implicit) {
final List<ResolverTuple> resolvers;
if (value.length() == 0) {
resolvers = yamlImplicitResolvers.get('\0');
} else {
// 获取字符串第一个字符,并获取对应的类型
// yamlImplicitResolvers保持了所有ASCII码和对应的可解析工具的列表。
resolvers = yamlImplicitResolvers.get(value.charAt(0));
}
// 获取到"1_2"中"1"的解析工具有两个,分别是整型和浮点两个解析工具
if (resolvers != null) {
// 遍历这两个解析类
for (ResolverTuple v : resolvers) {
Tag tag = v.getTag();
// 整型的正则匹配
Pattern regexp = v.getRegexp();
// 常量表达式Pattern.compile("^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$");
if (regexp.matcher(value).matches()) {
// 匹配成功,返回目标类型为int的tag
return tag;
}
}
}
// ...
}
//...
}
这里用正则匹配的方式把这个value的的tag标记为int的解析目标后,进行后续的把其转换为基本类型:
protected Object constructObjectNoCheck(Node node) {
// ...
// 获取对应的值解析工具,就是根据tag获取对应的转换工具
Construct constructor = getConstructor(node);
Object data = (constructedObjects.containsKey(node)) ? constructedObjects.get(node)
: constructor.construct(node);
finalizeConstruction(node, data);
constructedObjects.put(node, data);
recursiveObjects.remove(node);
if (node.isTwoStepsConstruction()) {
constructor.construct2ndStep(node, data);
}
return data;
}
匹配到了ConstructYamlInt之后:
public class ConstructYamlInt extends AbstractConstruct {
@Override
public Object construct(Node node) {
// 第一步就是把下划线替换掉
String value = constructScalar((ScalarNode) node).toString().replaceAll("_", "");
// ...
}
}
这里下划线就没了,后续就转换为了12。
解决方案
根据源码实现,只要给配置的值加双引号即可识别为字符串。
总结
在追踪问题根源的过程也是欣赏学习源码设计模式与代码风格的过程。框架的确是个好东西,但是理解的不到位,很多细节也是有很多坑。