一、现象【What】
某次在配置中心Nacos上配置了一个字符串,采用YAML格式,如下:
id: 114:1
代码中采用的是@Value注解的形式获取配置:
@Value("${id}")
代码中预期获取的是字符串:"114:1",但是日志打印出来的却是 6841。
二、原因排查【Why】
一开始,我以为是Nacos的问题,同事提醒说直接在application.yml文件中这么配置,解析出来的也是 6841,所以可以确定是SpringBoot解析的时候出的问题。我先尝试把114:1,加上双引号,
id: "114:1"
程序就可以正常获取到字符串"114:1"。
于是开始排查SpringBoot项目启动解析yml文件的过程,一步步断点,调试,发现SpringBoot项目是采用snakeyaml解析yml配置文件的。
SprintBoot版本:2.5.2,里面以来的 org.yaml.snakeyaml 版本:1.28
线程栈如下(从下往上):
我在application.yml中配置如下(id-2 作为 id 的比对):
server:
port: 8080
id: 114:1
id-2: "114:1"
在 org.yaml.snakeyaml.constructor.BaseConstructor 类的 constructObjectNoCheck(Node node) 方法,可以看到:
snakeyaml解析 id 对应的 valueNode 类型竟然是 int 类型,而作为 id-2 对应的是我们想要的字符串 str 类型。接着往下,SafeConstructor 类中的内部类 ConstructYamlInt 解析 int 类型的值:
public class ConstructYamlInt extends AbstractConstruct {
@Override
public Object construct(Node node) {
String value = constructScalar((ScalarNode) node).toString().replaceAll("_", "");
int sign = +1;
char first = value.charAt(0);
if (first == '-') {
sign = -1;
value = value.substring(1);
} else if (first == '+') {
value = value.substring(1);
}
int base = 10;
if ("0".equals(value)) {
return Integer.valueOf(0);
} else if (value.startsWith("0b")) {
value = value.substring(2);
base = 2;// 二进制
} else if (value.startsWith("0x")) {
value = value.substring(2);
base = 16;//十六进制
} else if (value.startsWith("0")) {
value = value.substring(1);
base = 8;//八进制
} else if (value.indexOf(':') != -1) {
//按秒解析冒号(:),这就是问题原因。
//配置的114:1被解析为6841
String[] digits = value.split(":");
int bes = 1;
int val = 0;
for (int i = 0, j = digits.length; i < j; i++) {
val += Long.parseLong(digits[j - i - 1]) * bes;
bes *= 60;
}
return createNumber(sign, String.valueOf(val), 10);
} else {
return createNumber(sign, value, 10);
}
return createNumber(sign, value, base);
}
}
这里判断是否有“:”,有的话会把数据按照时间单位秒数(60进制)进行解析返回。这里想不通为什么有这个秒数转换的实现。另外,还要注意的是,这里按“0x”开头的字符串如果不加引号,则按十六进制整数规则解析;“0”开头的字符串不加引号,则按八进制整数规则解析;“0b”开头的字符串不加引号,则按二进制整数规则解析;注意,以上前提是字符串能够按照对应的进制解析成功。
# 0x21 不加引号,解析为33
id: 0b11
# 021 不加引号,解析为17
id-2: 021
# 0b11 不加引号,解析为3
id-3: 0b11
# 0x2p2 不加引号,但无法按十六进制正常解析,按字符串解析的,解析为0x2p2
id: 0x2p2
三、怎么办(How)
遇到这样的带冒号的字符串,就老老实实把引号加上就可以了。