SpringBoot类属性”第二个字母大写“反序列化问题

SpringBoot类属性”第二个字母大写“反序列化问题

3 年前

前言

今天被同事问到一个序列化的问题,”在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方法,我们继续沿着断点运行

ObjectMapper.java

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


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

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);