关于FastJson long 溢出问题的小结

目录
  • 背景
    • 问题1. 对象转 json 字符串错误
    • 问题2. 对象转字节数组错误
  • 1 问题解析
  • 2 问题处理
    • 2.1 使用 ValueFilter 处理
    • 2.2 替换有问题的 IntegerCodec
    • 2.3 升级 FastJson
  • 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 并不生效。

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

(0)

相关推荐

  • 浅谈fastjson的常用使用方法

    如下所示: package Demo; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Vector; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; imp

  • 关于fastjson的@JSONField注解的一些问题(详解)

    @JSONField 看源码它可以作用于字段和方法上. 引用网上说的, 一.作用Field @JSONField作用在Field时,其name不仅定义了输入key的名称,同时也定义了输出的名称. 但是我在使用中,发现并不如上所说. 例如 @JSONField(name="project_id") private Long ProjectID 发现bean 转json的时候并是"project_id":xxx的形式,json转bean的时候也不会把"proj

  • java fastjson传输long数据却接收到了int的问题

    目录 fastjson传输long数据却接收到了int 使用FastJson做数据类型转换 数据转化为JSON 将json数据解析 fastjson传输long数据却接收到了int 最近在java开发中发现一个有趣现象,在网元A中的一个vo里有个类型为Long的taskId字段,用fastjson将其转为json字符串,用http请求传给网元B,网元B用一个map接收该实体,接收到后却发taskID其数据类型为Integer,如下图一所示. 此时如果用long taskId= (long) re

  • 关于FastJson long 溢出问题的小结

    目录 背景 问题1. 对象转 json 字符串错误 问题2. 对象转字节数组错误 1 问题解析 2 问题处理 2.1 使用 ValueFilter 处理 2.2 替换有问题的 IntegerCodec 2.3 升级 FastJson 3 小结 背景 严选项目中早期(2015年底)接入了 FastJson(版本 1.1.48.android),随着业务发展,个别请求字段数值超出 int 范围,暴露了 FastJson 当前版本的这个溢出问题. 当做总结,希望其他团队可以趁早规避这个坑 问题1. 对

  • Android 异步获取网络图片并处理导致内存溢出问题解决方法

    测试环境为Adnroid 2.1以上. 1.AndroidManifest.xml 权限配置: 添加互联网访问权限: 复制代码 代码如下: <uses-permission android:name="android.permission.INTERNET" /> 2.异步图片类 ImageDownloadTask 复制代码 代码如下: import java.io.ByteArrayOutputStream; import java.io.IOException; imp

  • Boostrap模态窗口的学习小结

    Bootstrap Modals(模态框)是使用定制的 Jquery 插件创建的.它可以用来创建模态窗口丰富用户体验,或者为用户添加实用功能.您可以在 Modals(模态框)中使用 Popover(弹出框)和 Tooltip(工具提示插件). 一.什么是模态窗口? 是这样一种效果哟: 二. 组成 头部(包括标题和关闭按钮) 中间(主要内容) 底部(主要是放置操作按钮) 对应在html代码中的布局是:使用div容器元素,分别使用modal,modal-dialog,modal-cotent样式,而

  • Java堆内存又溢出了!教你一招必杀技(推荐)

    JAVA堆内存管理是影响性能主要因素之一. 堆内存溢出是JAVA项目非常常见的故障,在解决该问题之前,必须先了解下JAVA堆内存是怎么工作的. 先看下JAVA堆内存是如何划分的,如图: 1.JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation).老年代(Old Generation),非堆内存就一个永久代(Permanent Generation). 2.年轻代又分为Eden和Survivor区.Survivor区由FromSpace和ToSpace组成.Eden

  • C++语言中std::array的用法小结(神器用法)

    摘要:在这篇文章里,将从各个角度介绍下std::array的用法,希望能带来一些启发. td::array是在C++11标准中增加的STL容器,它的设计目的是提供与原生数组类似的功能与性能.也正因此,使得std::array有很多与其他容器不同的特殊之处,比如:std::array的元素是直接存放在实例内部,而不是在堆上分配空间:std::array的大小必须在编译期确定:std::array的构造函数.析构函数和赋值操作符都是编译器隐式声明的--这让很多用惯了std::vector这类容器的程

  • C语言函数的基本使用和递归小结

    本章目标 秃头侠们好呀,今天我们一起学习函数! 目标: 本章主要掌握函数的基本使用和递归 函数是什么 数学中我们常见到函数的概念.但是你了解C语言中的函数吗? 维基百科中对函数的定义:子程序 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成.它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性. 一般

  • Redis的六种底层数据结构(小结)

    目录 1.简单动态字符串(SDS) 2.链表 3.字典 哈希表 哈希表节点 字典 4.跳跃表 跳跃表节点(zskiplistNode) 跳跃表(zskiplist) 5.整数集合 6.压缩列表 1.简单动态字符串(SDS) Redis 虽然是用 C 语言写的,但Redis没有直接使用C语言传统的字符串表示(以空字符 ‘\0’ 结尾的字符数组),二是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示.在R

  • C++无符号整数溢出问题解析

    目录 问题提出 设计思路 工程代码 测试 扩展知识 小结 本文主要探讨C/C++中无符号整数超过范围后的计算问题. 问题提出 nrf52832 的 SDK 中是没有时间戳获取的函数的,为了统计性能耗时,也为了向一些库提供时间戳(毫秒级别),需要自己利用定时器实现获取毫秒的接口. nrf52832 是 32 位的,按毫秒计算,大概49天就会达到最大值,如何处理毫秒数值溢出后的情况,其实我是不懂的.看了些帖子,说在单独处理溢出反转的情形,但总觉得这样不太好,因此集中了一点时间,了解学习了无符号数的溢

  • window下注册服务的命令小结

    1. 描述:     SC 是用于与服务控制管理器通信的命令行程序 .    用法:  sc <server> [command] [service name] <option1> <option2>... 选项 <server> 的格式为 可以键入 "sc [command]"以获得命令的进一步帮助 命令: query---------------查询服务的状态,或枚举服务类型的状态.           queryex--------

随机推荐