關於FastJson long 溢出問題的小結

背景

嚴選項目中早期(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 並不生效。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。 

推薦閱讀: