欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

关于FastJson long 溢出问题的小结

程序员文章站 2022-03-04 12:06:27
目录背景问题1. 对象转 json 字符串错误问题2. 对象转字节数组错误1 问题解析2 问题处理2.1 使用 valuefilter 处理2.2 替换有问题的 integercodec2.3 升级...

背景

严选项目中早期(2015年底)接入了 fastjson(版本 1.1.48.android),随着业务发展,个别请求字段数值超出 int 范围,暴露了 fastjson 当前版本的这个溢出问题。

当做总结,希望其他团队可以趁早规避这个坑

问题1. 对象转 json 字符串错误

在网络请求 response body 数据解析中,为了将 json 数据映射到对象上,调用了 json.tojsonstring() 方法,而这里的数据处理出现了 long 数据溢出,数据发生错误

object result = isarray ?
        json.parsearray(jsonobj.getjsonarray("data").tojsonstring(), modelcls) :
        jsonobj.getobject("data", modelcls);
parseresult.setresult(result);

数组对象映射代码看着有点怪,性能会有点浪费,因为涉及接口不多也没想到有更好的映射方式,就没改,轻喷。

问题2. 对象转字节数组错误

网络请求 request body 转字节数组过程,调用了 json.tojsonbytes 接口,而当 mbodymap 中存在 long 字段时发生了溢出。

@override
public byte[] getcontenteasbytes() {
    //防止重复转换
    if (mbody == null && mbodymap.size() != 0) {
        mbody = json.tojsonbytes(mbodymap);
    }
    return mbody;
}
//mbodymap 数据内容
map<string, object> mbodymap = new hashmap<>();
mbodymap.put("shipaddressid", 117645003002l);
...
invoicesubmitvo submit = new invoicesubmitvo();
submit.shipaddressid = 117645003002l;
mbodymap.put("invoicesubmite", submit);
//后端接收数据内容
{
    "invoicesubmite":{
        "shipaddressid": 117645003002,
        ...
    },
    "shipaddressid": 1680886010,    
    ...
}

同样的 2 个 long 字段 shipaddressid,一个能正常解析,一个发生了溢出。

1 问题解析

编写测试代码:

public static void test() {
    jsonobject jsonobj = new jsonobject();
    jsonobj.put("_int", 100);
    jsonobj.put("_long", 1234567890120l);
    jsonobj.put("_string", "string");
    string json0 = json.tojsonstring(jsonobj);
    log.i("test0", "json0 = " + json0);
        
    testmodel model = new testmodel();
    string json1 = json.tojsonstring(model);
    log.i("test1", "json1 = " + json1);
}
private static class testmodel {
    public int _int = 100;
    public long _long = 1234567890120l;
    public string _string = "string";
}

内容输出

i/test0: json0 = {"_int":100,"_long":1912276168,"_string":"string"}

i/test1: json1 = {"_int":100,"_long":1234567890120,"_string":"string"}

可以找到规律 map 中 long value 解析时,发生了溢出;而类对象中的 long 字段解析正常。

查看源码:

// json.java
public string tojsonstring() {
    serializewriter out = new serializewriter((writer)null, default_generate_feature, serializerfeature.empty);
    string var2;
    try {
        (new jsonserializer(out, serializeconfig.globalinstance)).write(this);
        var2 = out.tostring();
    } finally {
        out.close();
    }
    return var2;
}
    
public static final string tojsonstring(object object, serializerfeature... features) {
    serializewriter out = new serializewriter((writer)null, default_generate_feature, features);
    string var4;
    try {
        jsonserializer serializer = new jsonserializer(out, serializeconfig.globalinstance);
        serializer.write(object);
        var4 = out.tostring();
    } finally {
        out.close();
    }
    return var4;
}

可以看到,最终调用的都是 jsonserializer.write 方法

//jsonserializer.java
public final void write(object object) {
    ...
    objectserializer writer = this.getobjectwriter(clazz);
    ...
}
public objectserializer getobjectwriter(class<?> clazz) {
    objectserializer writer = (objectserializer)this.config.get(clazz);
    if (writer == null) {
        if(map.class.isassignablefrom(clazz)) {
            this.config.put(clazz, mapcodec.instance);
        }
        ...
        else {
            class superclass;
            if(!clazz.isenum() && ((superclass = clazz.getsuperclass()) == null || superclass == object.class || !superclass.isenum())) {
                if(clazz.isarray()) {
                    ...
                }
                ...
                else {
                    ...
                    this.config.put(clazz, this.config.createjavabeanserializer(clazz));
                }
            } else {
                ...
            }
        }
        writer = (objectserializer)this.config.get(clazz);
    }
    return writer;
}

可以看到 map 对象使用 mapcodec 处理,普通 class 对象使用 javabeanserializer 处理

mapcodec 处理序列化写入逻辑:

class<?> clazz = value.getclass();
if(clazz == preclazz) {
    prewriter.write(serializer, value, entrykey, (type)null);
} else {
    preclazz = clazz;
    prewriter = serializer.getobjectwriter(clazz);
    prewriter.write(serializer, value, entrykey, (type)null);
}

针对 long 字段的序列化类可以查看得到是 integercodec 类

// serializeconfig.java
public serializeconfig(int tablesize) {
    super(tablesize);
    ...
    this.put(byte.class, integercodec.instance);
    this.put(short.class, integercodec.instance);
    this.put(integer.class, integercodec.instance);
    this.put(long.class, integercodec.instance);
    ...
}

而查看 integercodec 源码就能看到问题原因:由于前面 fieldtype 写死 null 传入,导致最后写入都是 out.writeint(value.intvalue()); 出现了溢出。

\\integercodec.java
public void write(jsonserializer serializer, object object, object fieldname, type fieldtype) throws ioexception {
    serializewriter out = serializer.out;
    number value = (number)object;
    if(value == null) {
        ...
    } else {
        if (fieldtype != long.type && fieldtype != long.class) {
            out.writeint(value.intvalue());
        } else {
            out.writelong(value.longvalue());
        }
    }
}

而当 long 值是一个class 字段时,查看 javabeanserializer.write 方法,确实是被正确写入。

// javabeanserializer.java
public void write(jsonserializer serializer, object object, object fieldname, type fieldtype) throws ioexception {
    ...
    if(valuegot && !propertyvaluegot) {
        if(fieldclass != integer.type) {
            if(fieldclass == long.type) {
                serializer.out.writelong(propertyvaluelong);
            } else if(fieldclass == boolean.type) {
                ...
            }
        } else if(propertyvalueint == -2147483648) {
            ...
        }
        ...
    }
    ...
}

2 问题处理

2.1 使用 valuefilter 处理

针对 json.tojsonstring,可以调用如下方法,并设置 valuefilter,fastjson 在写入字符串之前会先调用 valuefilter.process 方法,在该方法中修改 value 的数据类型,从而绕开有 bug 的 integercodec 写入逻辑

public static final string tojsonstring(object object, serializefilter filter, serializerfeature... features)
public interface valuefilter extends serializefilter {
    object process(object object, string name, object value);
}
string json1 = json.tojsonstring(map, new valuefilter() {
    @override
    public object process(object object, string name, object value) {
        if (value instanceof long) {
            return new biginteger(string.valueof(value));
        }
        return value;
    }
});

这里修改 long 类型为 biginteger 类,而值不变,最后将写入操作交给 bigdecimalcodec

2.2 替换有问题的 integercodec

查看 serializeconfig 源码可以发现全部的 objectserializer 子类都集成在 serializeconfig 中,且内部使用 globalinstance

public class serializeconfig extends identityhashmap<objectserializer> {
    public static final serializeconfig globalinstance = new serializeconfig();
    public objectserializer createjavabeanserializer(class<?> clazz) {
        return new javabeanserializer(clazz);
    }
    public static final serializeconfig getglobalinstance() {
        return globalinstance;
    }
    public serializeconfig() {
        this(1024);
    }
    ...
}

为此可以在 application 初始化的时候替换 integercodec

//myapplication.java
@override
public void oncreate() {
    super.oncreate();    
    serializeconfig.getglobalinstance().put(byte.class, newintegercodec.instance);
    serializeconfig.getglobalinstance().put(short.class, newintegercodec.instance);
    serializeconfig.getglobalinstance().put(integer.class, newintegercodec.instance);
    serializeconfig.getglobalinstance().put(long.class, newintegercodec.instance);
}

由于 newintegercodec 用到的 serializewriter.features 字段是 protected,为此需要将该类放置在 com.alibaba.fastjson.serializer 包名下

2.3 升级 fastjson

现最新版本为 1.1.68.android(2018.07.16),查看 integercodec 类,可以发现 bug 已经修复

//integercodec.java
public void write(jsonserializer serializer, object object, object fieldname, type fieldtype) throws ioexception {
    ...
    
    if (object instanceof long) {
        out.writelong(value.longvalue());
    } else {
        out.writeint(value.intvalue());
    }    
    ...
}

综上看起来,最佳方案是升级 fastjson,然而升级过程中还是触发了其他的坑。

由于 nei 上定义的字段,部分数值变量定义类型为 number,同样的基本类型,后端字段部分采用了装箱类型,导致了和客户端定义类型不一致(如服务端定义 integer,客户端定义 int)。

public static void test() {
    string json = "{\"code\":200,\"msg\":\"\",\"data\":{\"_long\":1234567890120,\"_string\":\"string\",\"_int\":null}}";
    jsonobject jsonobj = jsonobject.parseobject(json);
    androidmodel androidmodel = jsonobj.getobject("data", androidmodel.class);
}
private static class androidmodel {
    public int _int = 100;
    public long _long = 1234567890120l;
    public string _string = "string";
}

如上测试代码,在早期版本这么定义并无问题,即便 _int 字段为 null,客户端也能解析成初始值 100。而升级 fastjson 之后,json 字符串解析就会发生崩溃

//javabeandeserializer.java
public object createinstance(map<string, object> map, parserconfig config) //
               throws illegalaccessexception,
               illegalargumentexception,
               invocationtargetexception {
    object object = null;
    
    if (beaninfo.creatorconstructor == null) {
        object = createinstance(null, clazz);
        
        for (map.entry<string, object> entry : map.entryset()) {
            ...
            if (method != null) {
                type paramtype = method.getgenericparametertypes()[0];
                value = typeutils.cast(value, paramtype, config);
                method.invoke(object, new object[] { value });
            } else {
                field field = fielddeser.fieldinfo.field;
                type paramtype = fielddeser.fieldinfo.fieldtype;
                value = typeutils.cast(value, paramtype, config);
                field.set(object, value);
            }
        }        
        return object;
    }
    ...
}
typeutils.java
@suppresswarnings("unchecked")
public static final <t> t cast(object obj, type type, parserconfig mapping) {
    if (obj == null) {
        return null;
    }
    ...
}

查看源码可以发现,当 json 字符串中 value 为 null 的时候,typeutils.cast 也直接返回 null,而在执行 field.set(object, value); 时,将 null 强行设置给 int 字段,就会发生 illegalargumentexception 异常。

而由于这个异常情况存在,导致客户端无法升级 fastjson

3 小结

以上便是我们严选最近碰到的问题,即便是 fastjson 这么有名的库,也存在这么明显debug,感觉有些吃惊。然而由于服务端和客户端 nei 上定义的字段类型不一致(装箱和拆箱类型),而导致 android 不能升级 fastjson,也警示了我们在 2 端接口协议等方面,必须要保持一致。

此外,上述解决方案 1、2,也仅仅解决了 json 序列化问题,而反序列化如 defaultjsonparser 并不生效。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。