严选项目中早期(2015年底)接入了 fastjson(版本 1.1.48.android),随着业务发展,个别请艺术门类求字段数值超出 int 范围,暴露了 fastjson 当前版本的这个溢出问题。
当做总结,希望其他团队可以趁早规避这个坑
在网络请求 respon body 数据解析中,为了将 json 数据映射到对象上,调用了 json.tojsonstring() 方法,而这里的数据处理出现了 long 数据溢出,数据发生错误
object result = isarray ? json.pararray(jsonobj.getjsonarray("data").tojsonstring(), modelcls) : jsonobj.getobject("data", modelcls);parresult.tresult(result);
数组对象映射代码看着有点怪,性能会有点浪费,因为涉及接口不多也没想到有更好的映射方式,就没改,轻喷。
网络请求 request body 转字节数组过程,调用了 json.tojsonbytes 接口,而当 mbodymap 中存在 long 字段时发生了溢出。
@overridepublic 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,一个能正常解析,一个发生了溢出。
编写测试代码:
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: jso休闲西装搭配n0 = {“_int”:100,”_long”:1912276168,”_string”:”string”}
i/test1: json1 = {“_int”:100,”_long”:1234567890120,”_string”:”string”}
可以找到规律 map 中 long value 解析时,发生了溢出;而类对象中的 long 字段解析正常。
查看源码:
// json.javapublic string tojsonstring() { rializewriter out = new rializewriter((writer)null, default_generate_feature, rializerfeature.empty); string var2; try { (new jsonrializer(out, rializeconfig.globalinstance)).write(this); var2 = out.tostring(); } finally { out.clo(); } return var2;} public static final string tojsonstring(object object, rializerfeature... features) { rializewriter out = new rializewriter((writer)null, default_generate_feature, features); string var4; try { jsonrializer rializer = new jsonrializer(out, rializeconfig.globalinstance); rializer.write(object); var4 = out.tostring(); } finally { out.clo(); } return var4;}
可以看到,最终调用的都是 jsonrializer.write 方法
//jsonrializer.javapublic final void write(object object) { ... objectrializer writer = this.getobjectwriter(clazz); ...}public objectrializer getobjectwriter(class<?> clazz) { objectrializer writer = (objectrializer)this.config.get(clazz); if (writer == null) { if(map.class.isassignablefrom(clazz)) { this.config.put(clazz, mapcodec.instance); } ... el { class superclass; if(!clazz.inum() && ((superclass = clazz.getsuperclass()) == null || superclass == object.class || !superclass.inum())) { if(clazz.isarray()) { ... } ... el { ... this.config.put(clazz, this.config.createjavabeanrializer(clazz)); } } el { ... } } writer = (objectrializer)this.config.get(clazz); } return writer;}
可以看到 map 对象使用 mapcodec 处理,普通 class 对象使用 javabeanrializer 处理
mapcodec 处理序列化写入逻辑:
class<?> clazz = value.getclass();i圆明园毁灭前的资料f(clazz == preclazz) { prewriter.write(rializer, value, entrykey, (type)null);} el { preclazz = clazz; prewriter = rializer.getobjectwriter(clazz); prewriter.write(rializer, value, entrykey, (type)null);}
针对 long 字段的序列化类可以查看得到是 integercodec 类
// rializeconfig.javapublic rializeconfig(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.javapublic void write(jsonrializer rializer, object object, object fieldname, type fieldtype) throws ioexception { rializewriter out = rializer.out; number value = (number)object; if(value == null) { ... } el { if (fieldtype != long.type && fieldtype != long.class) { out.writeint(value.intvalue()); } el { out.writelong(value.longvalue()); } }}
而当 long 值是一个class 字段时,查看 javabeanrializer.write 方法,确实是被正确写入。
// javabeanrializer.javapublic void write(jsonrializer rializer, object object, object fieldname, type fieldtype) throws ioexception { ... if(valuegot && !propertyvaluegot) { if(fieldclass != integer.type) { if(fieldclass == long.type) { rializer.out.writelong(propertyvaluelong); } el if(fieldclass == boolean.type) { ... } } el if(propertyvalueint == -2147483648) { ... } ... } ...}
针对 json.tojsonstring,可以调用如下方法,并设置 valuefilter,fastjson 在写入字符串之前会先调用 valuefilter.process 方法,在该方法中修改 value 的数据类型,从而绕开有 bug 的 integercodec 写入逻辑
public static final string tojsonstring(object object, rializefilter filter, rializerfeature... features)public interface valuefilter extends rializefilter { object process(object object, string name, object value);}string json1 = json.tojsonstring(map, new valuefilter() { @override have和has的用法public object process(object object, string name, object value) { if (value instanceof long) { return new biginteger(string.valueof(value)); } return value; }});
这里修改 long 类型为 biginteger 类,而值不变,最后将写入操作交给 bigdecimalcodec
查看 rializeconfig 源码可以发现全部的 objectrializer 子类都集成在 rializeconfig 中,且内部使用 globalinstance
public class rializeconfig extends identityhashmap<objectrializer> { public static final rializeconfig globalinstance = new rializeconfig(); public objectrializer createjavabeanrializer(class<?> clazz) { return new javabeanrializer(clazz); } public static final rializeconfig getglobalinstance() { return globalinstance; } public rializeconfig() { this(1024); } ...}
为此可以在 application 初始化的时候替换 integercodec
//myapplication.java@overridepublic void oncreate() { super.oncreate(); rializeconfig.getglobalinstance().put(byte.class, newintegercodec.instance); rializ励志流行歌曲econfig.getglobalinstance().put(short.class, newintegercodec.instance); rializeconfig.getglobalinstance().put(integer.class, newintegercodec.instance); rializeconfig.getglobalinstance().put(long.class, newintegercodec.instance);}
由于 newintegercodec 用到的 rializewriter.features 字段是 protected,为此需要将该类放置在 com.alibaba.fastjson.rializer 包名下
现最新版本为 1.1.68.android(2018.07.16),查看 integercodec 类,可以发现 bug 已经修复
//integercodec.javapublic void write(jsonrializer rializer, object object, object fieldname, type fieldtype) throws ioexception { ... if (object instanceof long) { out.writelong(value.longvalue()); } el { 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.parobject(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 字符串解析就会发生崩溃
//javabeanderializer.javapublic object createinstance(map<string, object> map, parrconfig config) // throws illegalaccesxception, illegalargumentexception, invocationtargetexception { object object = null; if (beaninfo.creatorconstructor == null) { object = createinstance(null, clazz); for (map.entry<string, object> entry : map.entryt()) { ... if (method != null) { type paramtype = method.getgenericparametertypes()[0]; value = typeutils.cast(value, paramtype, config); method.invoke(object, new object[] { value }); } el { field field = fieldder.fieldinfo.field; type paramtype = fieldder.fieldinfo.fieldtype; value = typeutils.cast(value, paramtype, config); field.t(object, value); } } return object; } ...}typeutils.java@suppresswarnings("unchecked")public static final <t> t cast(object obj, type type, parrconfig mapping) { if (obj == null) { return null; } ...}
查看源码可以发现,当 json 字符串中 value 为 null 的时候,typeutils.cast 也直接返回 null,而在执行 field.t(object, value); 时,将 null 强行设置给 int 字段,就会发生 illegalargumentexception 异常。
而由于这个异常情况存在,导致客户端无法升级 fastjson
以上便是我们严选最近碰到的问题,即便是 fastjson 这么有名的库,也存在这么明显debug,感觉有些吃惊。然而由于服务端和客户端 nei 上定义的字段类型不一致(装箱和拆箱类型),而导致 android 不能升级 fastjson,也警示了我们在 2 端接口协议等方面,必须要保持一致。
此外,上述解决方案 1、2,也仅仅解决了 json 序列化问题,而反序列化如 defaultjsonparr 并不生效。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持www.887551.com。
本文发布于:2023-04-04 14:18:17,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/fanwen/zuowen/3727fa8e12af96a8e15193ab6fe884c9.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:关于FastJson long 溢出问题的小结.doc
本文 PDF 下载地址:关于FastJson long 溢出问题的小结.pdf
留言与评论(共有 0 条评论) |