json 字段变成小写 序列化_SpringBoot类属性”第二个字母大写“反序列化问题

前言

今天被同事问到一个序列化的问题,”在SpringBoot默认序列化的情况下,Web请求的JSON字段包含首字母小写第二个字母大写的变量名无法解析(aName)“。在经过多次尝试不同的命名规则(aaName、aaa)等均可反序列化。由于SpringBoot默认采用Jackson作为序列化工具,所以猜测是由于Jackson反序列化时有一些小bug。

复现

SpringBoot:2.1.8.RELEASE

Jackson Version:2.9.9

Web JSON:

{
    "aName":"jackson"
}

Java Object:

public class Test {
    private String aName;
}

Controller:

@PostMapping("test")
public void testDeserialization(@RequestBody Test test) {
    System.out.println("反序列化为:" + test.getAName());
}

调用该接口发现控制台打印结果:“反序列化为:null”

排查

因为SpringBoot使用Jackson进行序列化与反序列化。我们直接看AbstractJackson2HttpMessageConverter类下的readJavaType()方法,该方法就是把输入消息转换为对象。

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
    try {
        if (inputMessage instanceof MappingJacksonInputMessage) {
            Class deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
            if (deserializationView != null) {
                return this.objectMapper.readerWithView(deserializationView).forType(javaType).
                        readValue(inputMessage.getBody());
            }
        }
        return this.objectMapper.readValue(inputMessage.getBody(), javaType);
    }
    catch (InvalidDefinitionException ex) {
        throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
    }
    catch (JsonProcessingException ex) {
        throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
    }
}

根据断点运行到第二个return方法,我们继续沿着断点运行

json 字段变成小写 序列化_SpringBoot类属性”第二个字母大写“反序列化问题_第1张图片
ObjectMapper.java

在断点这一行上方根据JavaType(Test类)获取到了对应的解析器。在反序列化器BeanDeserializer中有一个_beanProperties的属性中可以看到一个“aname”的属性。这里就有些奇怪,我们在入参的json和Bean定义中都没有这个属性。所以几乎确定是因为Jackson在通过setAName()方法获取属性名的时候将aName设置为了aname,导致JSON入参的aName找不到对应的属性,反序列化为空。

确定了是反序列化类的问题后,我们就来看看Jackson是如何根据setAName方法。根据层层的debug,定位到POJOPropertiesCollector类的collectAll方法。这个方法是获取对象映射的所有属性。

json 字段变成小写 序列化_SpringBoot类属性”第二个字母大写“反序列化问题_第2张图片
POJOPropertiesController.java
_addFields(props);
_addMethods(props);

这两个方法过后可以看出根据属性值获取到“aName”,但是根据methods(setAName)获取到是“aname”。

_removeUnwantedProperties(props);

通过上面的方法又剔除了“aName”的属性值,可以看出Jackson是根据setter方法来确定属性的

接下来我们debug进入_addMethods(props)方法,最终找到BeanUtil类下的legacyManglePropertyName方法。Debug得到的入参basename为“setAName”、offset为“3”(从“AName”开始)。

protected static String legacyManglePropertyName(final String basename, final int offset) {
    final int end = basename.length();
    if (end == offset) { // empty name, nope
        return null;
    }
    // next check: is the first character upper case? If not, return as is
    char c = basename.charAt(offset);
    char d = Character.toLowerCase(c);
    
    if (c == d) {
        return basename.substring(offset);
    }
    // otherwise, lower case initial chars. Common case first, just one char
    StringBuilder sb = new StringBuilder(end - offset);
    sb.append(d);
    int i = offset+1;
    for (; i < end; ++i) {
        c = basename.charAt(i);
        d = Character.toLowerCase(c);
        if (c == d) {
            sb.append(basename, i, end);
            break;
        }
        sb.append(d);
    }
    return sb.toString();
}

可以看到最下方的StringBuilder就是最终获取的属性值,sb首先加入d("a",原offset为3时已转换小写),然后把偏移量向后+1。在第一次循环中的c和d分别是“N”和“n”。

这里有一个比较奇怪的判断:如果c == d,则直接拼接后续的字符串,跳出循环;否则,将小写字符拼接后,继续遍历。

第一次遍历后sb的值为“an”。

第二次遍历,由于c == d(“a”==“a”),所以直接拼接后续字符串。

得到sb的最终结果为“aname”。

结论

由于没有通篇研读Jackson的代码、并不能确定这里的for循环为什么以这种方式处理方法名。

但是可以提供给大家两个解决方案:

  • 使用其他JSON序列化包替换Jackson的MessageConverter。
  • 使用@JsonProperty("aname")注解类属性,指定错误反序列化后的属性名。

你可能感兴趣的:(json,字段变成小写,序列化)