[关闭]
@zyl06 2018-09-21T17:41:28.000000Z 字数 7942 阅读 1789

FastJson long 溢出问题小结

FastJson Android


0 背景

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

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

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

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

  1. Object result = isArray ?
  2. JSON.parseArray(jsonObj.getJSONArray("data").toJSONString(), modelCls) :
  3. jsonObj.getObject("data", modelCls);
  4. parseResult.setResult(result);

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

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

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

  1. @Override
  2. public byte[] getContenteAsBytes() {
  3. //防止重复转换
  4. if (mBody == null && mBodyMap.size() != 0) {
  5. mBody = JSON.toJSONBytes(mBodyMap);
  6. }
  7. return mBody;
  8. }
  1. //mBodyMap 数据内容
  2. Map<String, Object> mBodyMap = new HashMap<>();
  3. mBodyMap.put("shipAddressId", 117645003002L);
  4. ...
  5. InvoiceSubmitVO submit = new InvoiceSubmitVO();
  6. submit.shipAddressId = 117645003002L;
  7. mBodyMap.put("invoiceSubmite", submit);
  8. //后端接收数据内容
  9. {
  10. "invoiceSubmite":{
  11. "shipAddressId": 117645003002,
  12. ...
  13. },
  14. "shipAddressId": 1680886010,
  15. ...
  16. }

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

1 问题解析

编写测试代码:

  1. public static void test() {
  2. JSONObject jsonObj = new JSONObject();
  3. jsonObj.put("_int", 100);
  4. jsonObj.put("_long", 1234567890120L);
  5. jsonObj.put("_string", "string");
  6. String json0 = JSON.toJSONString(jsonObj);
  7. Log.i("TEST0", "json0 = " + json0);
  8. TestModel model = new TestModel();
  9. String json1 = JSON.toJSONString(model);
  10. Log.i("TEST1", "json1 = " + json1);
  11. }
  12. private static class TestModel {
  13. public int _int = 100;
  14. public long _long = 1234567890120L;
  15. public String _string = "string";
  16. }

内容输出

  1. I/TEST0: json0 = {"_int":100,"_long":1912276168,"_string":"string"}
  2. I/TEST1: json1 = {"_int":100,"_long":1234567890120,"_string":"string"}

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

查看源码:

  1. // JSON.java
  2. public String toJSONString() {
  3. SerializeWriter out = new SerializeWriter((Writer)null, DEFAULT_GENERATE_FEATURE, SerializerFeature.EMPTY);
  4. String var2;
  5. try {
  6. (new JSONSerializer(out, SerializeConfig.globalInstance)).write(this);
  7. var2 = out.toString();
  8. } finally {
  9. out.close();
  10. }
  11. return var2;
  12. }
  13. public static final String toJSONString(Object object, SerializerFeature... features) {
  14. SerializeWriter out = new SerializeWriter((Writer)null, DEFAULT_GENERATE_FEATURE, features);
  15. String var4;
  16. try {
  17. JSONSerializer serializer = new JSONSerializer(out, SerializeConfig.globalInstance);
  18. serializer.write(object);
  19. var4 = out.toString();
  20. } finally {
  21. out.close();
  22. }
  23. return var4;
  24. }

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

  1. //JSONSerializer.java
  2. public final void write(Object object) {
  3. ...
  4. ObjectSerializer writer = this.getObjectWriter(clazz);
  5. ...
  6. }
  7. public ObjectSerializer getObjectWriter(Class<?> clazz) {
  8. ObjectSerializer writer = (ObjectSerializer)this.config.get(clazz);
  9. if (writer == null) {
  10. if(Map.class.isAssignableFrom(clazz)) {
  11. this.config.put(clazz, MapCodec.instance);
  12. }
  13. ...
  14. else {
  15. Class superClass;
  16. if(!clazz.isEnum() && ((superClass = clazz.getSuperclass()) == null || superClass == Object.class || !superClass.isEnum())) {
  17. if(clazz.isArray()) {
  18. ...
  19. }
  20. ...
  21. else {
  22. ...
  23. this.config.put(clazz, this.config.createJavaBeanSerializer(clazz));
  24. }
  25. } else {
  26. ...
  27. }
  28. }
  29. writer = (ObjectSerializer)this.config.get(clazz);
  30. }
  31. return writer;
  32. }

可以看到 Map 对象使用 MapCodec 处理,普通 Class 对象使用 JavaBeanSerializer 处理

MapCodec 处理序列化写入逻辑:

  1. Class<?> clazz = value.getClass();
  2. if(clazz == preClazz) {
  3. preWriter.write(serializer, value, entryKey, (Type)null);
  4. } else {
  5. preClazz = clazz;
  6. preWriter = serializer.getObjectWriter(clazz);
  7. preWriter.write(serializer, value, entryKey, (Type)null);
  8. }

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

  1. // SerializeConfig.java
  2. public SerializeConfig(int tableSize) {
  3. super(tableSize);
  4. ...
  5. this.put(Byte.class, IntegerCodec.instance);
  6. this.put(Short.class, IntegerCodec.instance);
  7. this.put(Integer.class, IntegerCodec.instance);
  8. this.put(Long.class, IntegerCodec.instance);
  9. ...
  10. }

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

  1. \\IntegerCodec.java
  2. public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType) throws IOException {
  3. SerializeWriter out = serializer.out;
  4. Number value = (Number)object;
  5. if(value == null) {
  6. ...
  7. } else {
  8. if (fieldType != Long.TYPE && fieldType != Long.class) {
  9. out.writeInt(value.intValue());
  10. } else {
  11. out.writeLong(value.longValue());
  12. }
  13. }
  14. }

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

  1. // JavaBeanSerializer.java
  2. public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType) throws IOException {
  3. ...
  4. if(valueGot && !propertyValueGot) {
  5. if(fieldClass != Integer.TYPE) {
  6. if(fieldClass == Long.TYPE) {
  7. serializer.out.writeLong(propertyValueLong);
  8. } else if(fieldClass == Boolean.TYPE) {
  9. ...
  10. }
  11. } else if(propertyValueInt == -2147483648) {
  12. ...
  13. }
  14. ...
  15. }
  16. ...
  17. }

2 问题处理

2.1 使用 ValueFilter 处理

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

  1. public static final String toJSONString(Object object, SerializeFilter filter, SerializerFeature... features)
  1. public interface ValueFilter extends SerializeFilter {
  2. Object process(Object object, String name, Object value);
  3. }
  1. String json1 = JSON.toJSONString(map, new ValueFilter() {
  2. @Override
  3. public Object process(Object object, String name, Object value) {
  4. if (value instanceof Long) {
  5. return new BigInteger(String.valueOf(value));
  6. }
  7. return value;
  8. }
  9. });

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

2.2 替换有问题的 IntegerCodec

查看 SerializeConfig 源码可以发现全部的 ObjectSerializer 子类都集成在 SerializeConfig 中,且内部使用 globalInstance

  1. public class SerializeConfig extends IdentityHashMap<ObjectSerializer> {
  2. public static final SerializeConfig globalInstance = new SerializeConfig();
  3. public ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
  4. return new JavaBeanSerializer(clazz);
  5. }
  6. public static final SerializeConfig getGlobalInstance() {
  7. return globalInstance;
  8. }
  9. public SerializeConfig() {
  10. this(1024);
  11. }
  12. ...
  13. }

为此可以在 Application 初始化的时候替换 IntegerCodec

  1. //MyApplication.java
  2. @Override
  3. public void onCreate() {
  4. super.onCreate();
  5. SerializeConfig.getGlobalInstance().put(Byte.class, NewIntegerCodec.instance);
  6. SerializeConfig.getGlobalInstance().put(Short.class, NewIntegerCodec.instance);
  7. SerializeConfig.getGlobalInstance().put(Integer.class, NewIntegerCodec.instance);
  8. SerializeConfig.getGlobalInstance().put(Long.class, NewIntegerCodec.instance);
  9. }

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

2.3 升级 FastJson

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

  1. //IntegerCodec.java
  2. public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType) throws IOException {
  3. ...
  4. if (object instanceof Long) {
  5. out.writeLong(value.longValue());
  6. } else {
  7. out.writeInt(value.intValue());
  8. }
  9. ...
  10. }

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

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

  1. public static void test() {
  2. String json = "{\"code\":200,\"msg\":\"\",\"data\":{\"_long\":1234567890120,\"_string\":\"string\",\"_int\":null}}";
  3. JSONObject jsonObj = JSONObject.parseObject(json);
  4. AndroidModel AndroidModel = jsonObj.getObject("data", AndroidModel.class);
  5. }
  6. private static class AndroidModel {
  7. public int _int = 100;
  8. public long _long = 1234567890120L;
  9. public String _string = "string";
  10. }

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

  1. //JavaBeanDeserializer.java
  2. public Object createInstance(Map<String, Object> map, ParserConfig config) //
  3. throws IllegalAccessException,
  4. IllegalArgumentException,
  5. InvocationTargetException {
  6. Object object = null;
  7. if (beanInfo.creatorConstructor == null) {
  8. object = createInstance(null, clazz);
  9. for (Map.Entry<String, Object> entry : map.entrySet()) {
  10. ...
  11. if (method != null) {
  12. Type paramType = method.getGenericParameterTypes()[0];
  13. value = TypeUtils.cast(value, paramType, config);
  14. method.invoke(object, new Object[] { value });
  15. } else {
  16. Field field = fieldDeser.fieldInfo.field;
  17. Type paramType = fieldDeser.fieldInfo.fieldType;
  18. value = TypeUtils.cast(value, paramType, config);
  19. field.set(object, value);
  20. }
  21. }
  22. ...
  23. return object;
  24. }
  25. ...
  26. }
  1. TypeUtils.java
  2. @SuppressWarnings("unchecked")
  3. public static final <T> T cast(Object obj, Type type, ParserConfig mapping) {
  4. if (obj == null) {
  5. return null;
  6. }
  7. ...
  8. }

查看源码可以发现,当 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 并不生效。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注