Jackson序列化之自动检测

  • Post author:
  • Post category:其他


一、背景

今天线上出现了一个问题,使用springMVC RestController接口返回json数据给客户端,发现其中某一个model中的所有属性,被序列化了两遍,并且一次是大写开头,一次是小写,部分结构如下:

                "promotionTags": [
                    {
                        "CornerRadius": 1,
                        "TitleFontSize": 10,
                        "Title": "返券",
                        "TitleColor": "#FF9900",
                        "Transparent": true,
                        "BackgroundColor": "#FF9900",
                        "Border": true,
                        "Transparent": true,
                        "Border": true,
                        "cornerRadius": 1,
                        "titleFontSize": 10,
                        "title": "返券",
                        "titleColor": "#FF9900",
                        "transparent": true,
                        "backgroundColor": "#FF9900",
                        "border": true,
                        "transparent": true,
                        "border": true
                    }
                ]

model结构如下:

public class HotelLabelModel implements Serializable {

    private Double CornerRadius;

    private String BorderColor;

    private Integer TitleFontSize;

    private String Title;

    private String TitleColor;

    private Boolean Transparent;

    private String BackgroundColor;

    private Boolean Border;

    getter and setter ...
}

对springMVC序列化做了简单的配置,如下:

public class HotelMappingJacksonHttpMessageConverter extends MappingJackson2HttpMessageConverter {

    public HotelMappingJacksonHttpMessageConverter() {
        super();
        this.setSelfConfiguration();
    }

    public HotelMappingJacksonHttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper);
        this.setSelfConfiguration();
    }

    private void setSelfConfiguration() {
        // 任何属性可见
        super.getObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
        // 过滤null
        super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }
}

二、验证

问题比较容易还原,简单demo如下:

public class JacksonTest {

    public static void main(String[] args) throws JsonProcessingException {
        //name content age
        UserBean userBean = new UserBean("Li Lei", "I am Li Lei", 20);

        //jackson序列化
        ObjectMapper objectMapper = new ObjectMapper();
        //设置任何字段可见
        objectMapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
        System.out.println(objectMapper.writeValueAsString(userBean));
    }
}
public class UserBean {

    private String Name;

    private String Content;

    private Integer Age;

    public UserBean(String name, String content, Integer age) {
        Name = name;
        Content = content;
        Age = age;
    }

    getter and setter ...
} 
输出结果:{"Name":"Li Lei","Content":"I am Li Lei","Age":20,"name":"Li Lei","content":"I am Li Lei","age":20}

跟踪源码,在类POJOPropertiesCollector中发现

protected void collectAll(){
        LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();

        // First: gather basic data
        _addFields(props);
        _addMethods(props);
       ...
}

属性序列化会判断属性的访问权限,在此就不大量的贴源码了

protected void _addFields(Map<String, POJOPropertyBuilder> props)
    {
            ...
            // having explicit name means that field is visible; otherwise need to check the rules
            boolean visible = (pn != null);
            if (!visible) {
                visible = _visibilityChecker.isFieldVisible(f);
            }
            ...
    }

根据get/set方法来序列化

protected void _addMethods(Map<String, POJOPropertyBuilder> props)
    {
        final AnnotationIntrospector ai = _annotationIntrospector;
        
        for (AnnotatedMethod m : _classDef.memberMethods()) {
            
            int argCount = m.getParameterCount();
            if (argCount == 0) { // getters (including 'any getter')
            	_addGetterMethod(props, m, ai);
            } else if (argCount == 1) { // setters
            	_addSetterMethod(props, m, ai);
            } else if (argCount == 2) { // any getter?
                if (ai != null  && ai.hasAnySetterAnnotation(m)) {
                    if (_anySetters == null) {
                        _anySetters = new LinkedList<AnnotatedMethod>();
                    }
                    _anySetters.add(m);
                }
            }
        }
    }

如下get方法,通过反射拿到方法名,截取get后面的名称toLowerCase后,作为序列化的name

public static String okNameForRegularGetter(AnnotatedMethod am, String name,
            boolean stdNaming)
    {
        if (name.startsWith("get")) {
            
            if ("getCallbacks".equals(name)) {
                if (isCglibGetCallbacks(am)) {
                    return null;
                }
            } else if ("getMetaClass".equals(name)) {
                // 30-Apr-2009, tatu: Need to suppress serialization of a cyclic reference
                if (isGroovyMetaClassGetter(am)) {
                    return null;
                }
            }
            return stdNaming
                    ? stdManglePropertyName(name, 3)
                    : legacyManglePropertyName(name, 3);
        }
        return null;
    } 
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();
    }

jackson是根据反射获取model的属性和get/set方法,序列化的顺序为字段、方法(get/set),默认只序列化public修饰的字段和public修饰的get/set方法。

所以上面输出结果输出了两遍的结论是第一遍输出的是属性的序列化内容,第二遍输出的是get方法的序列化内容。

定位到了问题,解决方案就比较简单了,方案有很多,如:换屏蔽掉get/set方法的序列化、换用Google的Gson序列化等等,为了减少风险,我选用了第一种方案。

demo修改如下:

public class JacksonTest {

    public static void main(String[] args) throws JsonProcessingException {
        UserBean userBean = new UserBean("Li Lei", "I am Li Lei", 20);

        //jackson序列化
        ObjectMapper objectMapper = new ObjectMapper();
        //屏蔽get方法的序列化
        objectMapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);
        //设置任何属性可见
        objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        System.out.println(objectMapper.writeValueAsString(userBean));
    }
}
输出结果:{"Name":"Li Lei","Content":"I am Li Lei","Age":20}

三、解决方案

项目中修改springMVC的配置信息,如下:

public class HotelMappingJacksonHttpMessageConverter extends MappingJackson2HttpMessageConverter {

    public HotelMappingJacksonHttpMessageConverter() {
        super();
        this.setSelfConfiguration();
    }

    public HotelMappingJacksonHttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper);
        this.setSelfConfiguration();
    }

    private void setSelfConfiguration() {
        // 任何属性可见
        super.getObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
        // 屏蔽get方法
        super.getObjectMapper().setVisibility(PropertyAccessor.GETTER, Visibility.NONE);
        // 屏蔽null
        super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }
}

预发布环境测试通过,上线。

四、总结

jackson默认的检测机制如下:public修饰的字段->public修饰的getter/的setter,在使用jackson作为序列化工具的时候要注意属性和方法的修饰权限。

选型jackson是考虑稳定性和性能的平衡点,Fastjson bug比较多,gson非常强大但效率比较低,如对序列化没有特殊要求尽量选用gson或gson & jackson结合使用。