0 背景
严选项目中早期(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 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 {
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 map, ParserConfig config) //
throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException {
Object object = null;
if (beanInfo.creatorConstructor == null) {
object = createInstance(null, clazz);
for (Map.Entry 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 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
并不生效。