Android FastJson与不规范JSON引发的血案

去公司实习了,没多少时间更博客了,距离上一篇博客也有一个来月了。看标题,应该可以看出,这篇文章是讲一个坑,以及如何填坑。

坑是什么?有两个坑,其一是fastjson的bug,其二是不规范的json字符串。如何填坑,不要着急,后文详细说明。

首先,我们看一个json字符串

{
    "doubleParam": 4.875,
    "floatParam": 2.76,
    "extra": {
        "doubleParam": 12.23,
        "floatParam": 12.54
    },
}

这是一个比较规范的json字符串,如果我们使用fastjson解析它,基本上是不会有任何问题的。

解析之前,我们需要根据这个json字符串生成bean,当然你可以选择Android Studio中的插件Gson Formattor自动生成bean。最终的两个bean如下所示,为了代码简单,我直接将属性设为了public,省略了getter和setter方法。

public class Extra {
    public double doubleParam;
    public float floatParam;
    @Override
    public String toString() {
        return "Extra{" +
                "doubleParam=" + doubleParam +
                ", floatParam=" + floatParam +
                ‘}‘;
    }
}

public class Bean {
    public double doubleParam;
    public float floatParam;
    public Extra extra;

    @Override
    public String toString() {
        return "Bean{" +
                "doubleParam=" + doubleParam +
                ", floatParam=" + floatParam +
                ", extra=" + extra +
                ‘}‘;
    }
}

然后我们需要一个泛型的Json工具类,对于这个工具类,你要做的就是直接使用即可。

public class JsonUtil {
    /**
     * 将对象转换成json
     *
     * @param src 对象
     * @return 返回json字符串
     * @throws Exception
     */
    public static <T> String toJson(T src) {

        try {
            return src instanceof String ? (String) src : JSON.toJSONString(src);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json通过类型转换成对象
     *
     * @param json  json字符串
     * @param clazz 泛型类型
     * @return 返回对象, 失败返回NULL
     */
    public static <T> T fromJson(String json, Class<T> clazz) {

        try {
            return clazz.equals(String.class) ? (T) json : JSON.parseObject(json, clazz);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 将json通过类型转换成对象
     *
     * @param json          json字符串
     * @param typeReference 引用类型
     * @return 返回对象
     */
    public static <T> T fromJson(String json, TypeReference<?> typeReference) {

        try {
            return (T) (typeReference.getType().equals(String.class) ? json
                    : JSON.parseObject(json, typeReference));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

我们将这个json字符串定义成一个常量,方便使用。

public class Constant {
    public static final String JSON = "{\"doubleParam\":4.875,\"extra\":{\"doubleParam\":12.23,\"floatParam\":12.54},\"floatParam\":2.76}";
}

接下来就是开始解析了,直接使用JsonUtil工具类进行解析即可。

Bean bean = JsonUtil.fromJson(Constant.JSON, Bean.class);
System.out.println(bean);

只要能够正常的输出结果,基本上就是解析成功了。输出结果如下

Bean{doubleParam=4.875, floatParam=2.76, extra=Extra{doubleParam=12.23, floatParam=12.54}}

以上过程,看上去十分完美,但是现实往往是残酷的。第一个坑即将来临。客户端使用的json字符串往往是来自服务器端,而客户端与服务器端的json规范有时候往往没有统一,导致了一方必然需要兼容另一方。比如,服务器端返回的json字符串,无论是否是字符串类型,有时候都会加上一个引号,即使你的属性值是int,float,double,boolean类型,服务器端给你返回的也是一个字符串。 就像下面这样

{
    "doubleParam": "4.875",
    "extra": {
        "doubleParam": "12.23",
        "floatParam": "12.54"
    },
    "floatParam": "2.76"
}

上面这个json字符串其实是不规范的,因为不论什么数据类型,它都加上了引号,好在使用fastjson解析这样的数据时,也是可以正常解析的。而且解析结果十分完美,没有一丝的错误。那么为什么说是个坑呢。

服务器返回上面这种都是字符串类型的其实都好说,不会对解析结果造成任何影响,但是往往现实也不是这样的,有时候某一个值没有,服务器理论上应该返回一个默认值,比如int类型应该返回”0”,然而,实际上你接受到的json字符串却是这样的。

{
    "doubleParam": "",
    "extra": {
        "doubleParam": "12.23",
        "floatParam": ""
    },
    "floatParam": "2.76"
}

可以看到,最外层的doubleParam值为空字符串,内部的extra对象中的floatParam值也为空字符串。其实即使是空,它们也应该被赋值一个默认值0,这样,在客户端解析时不会发生任何错误。问题就出在这个空字符串。一旦使用了空字符串,在客户端解析时就会引发致命的错误。而这个错误的原因来自fastjson的一个Bug,然而我并不知道作者为什么没有修复这个bug。现在我们来解析一下,看看会发生什么错误。代码还是原来的配方。但是结果却发生了翻天覆地的变化。程序报了一个错误,并且最终我们解析得到的bean是一个null。

com.alibaba.fastjson.JSONException: set property error, doubleParam
    at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:97)
    at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:43)
    at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField(JavaBeanDeserializer.java:400)
    at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:310)
    at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:115)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:548)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:215)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:191)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:150)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:255)
    at cn.edu.zafu.demo.fastjson.JsonUtil.fromJson(JsonUtil.java:66)
    at cn.edu.zafu.demo.Main.main(Main.java:9)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.IllegalArgumentException: Can not set double field cn.edu.zafu.demo.bean.Bean.doubleParam to null value
    at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
    at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
    at sun.reflect.UnsafeDoubleFieldAccessorImpl.set(UnsafeDoubleFieldAccessorImpl.java:80)
    at java.lang.reflect.Field.set(Field.java:764)
    at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:95)
    ... 16 more
null

发生错误的原因这里不追究,如果你想知道原因,跟进源码查看一下你就会知道为什么会产生这个异常。当前,我使用的fastjson的版本是

compile ‘com.alibaba:fastjson:1.1.46.android‘

应该是android中的最新版,而且我也查过了官方的wiki中的release日志,在1.1.45版本里面明确说明了修复过这个bug,但是实际上还是存在。

修复数值类型byte、short、int、long、float、double、boolean的值为null时,反序列化失败的问题。

确实,其他数据类型,比如int,long,boolean是不在在这个问题的,唯独float和double存在这个问题。于是我们需要对症下药。

我们来翻翻fastjson的源码。在parser包下面有一个ParserConfig,里面有这么一段代码。

public FieldDeserializer createFieldDeserializer(ParserConfig mapping, Class<?> clazz, FieldInfo fieldInfo) {
        Class<?> fieldClass = fieldInfo.getFieldClass();

        if (fieldClass == boolean.class || fieldClass == Boolean.class) {
            return new BooleanFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == int.class || fieldClass == Integer.class) {
            return new IntegerFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == long.class || fieldClass == Long.class) {
            return new LongFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == String.class) {
            return new StringFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == List.class || fieldClass == ArrayList.class) {
            return new ArrayListTypeFieldDeserializer(mapping, clazz, fieldInfo);
        }

        return new DefaultFieldDeserializer(mapping, clazz, fieldInfo);
    }

可以看到,boolean,int,long都设置了反序列化用的类,而float和double却没有,于是,我们就需要对这个方法进行改造。我们新建一个PatchParserConfig类继承ParserConfig类。我们预料的结果应该是这样的。

public class PatchParserConfig extends ParserConfig {

    @Override
    public FieldDeserializer createFieldDeserializer(ParserConfig mapping, Class<?> clazz, FieldInfo fieldInfo) {
        Class<?> fieldClass = fieldInfo.getFieldClass();

        if (fieldClass == boolean.class || fieldClass == Boolean.class) {
            return new BooleanFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == int.class || fieldClass == Integer.class) {
            return new IntegerFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == long.class || fieldClass == Long.class) {
            return new LongFieldDeserializer(mapping, clazz, fieldInfo);
        }

        // patch
        if (fieldClass == float.class || fieldClass == Float.class) {
            return new FloatFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == double.class || fieldClass == Double.class) {
            return new DoubleFieldDeserializer(mapping, clazz, fieldInfo);
        }
        // patch

        if (fieldClass == String.class) {
            return new StringFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == List.class || fieldClass == ArrayList.class) {
            return new ArrayListTypeFieldDeserializer(mapping, clazz, fieldInfo);
        }

        return new DefaultFieldDeserializer(mapping, clazz, fieldInfo);
    }
}

很明显的我们增加了下面的几行代码

// patch
        if (fieldClass == float.class || fieldClass == Float.class) {
            return new FloatFieldDeserializer(mapping, clazz, fieldInfo);
        }

        if (fieldClass == double.class || fieldClass == Double.class) {
            return new DoubleFieldDeserializer(mapping, clazz, fieldInfo);
        }
        // patch

而FloatFieldDeserializer类和DoubleFieldDeserializer都是哪来的呢。这个很简单,照样画葫芦就可以了,在parser包里的deserializer包种有各种反序列化用的类。我们选择其中一个照样画葫芦就ok了。比如我们选择LongFieldDeserializer,其代码如下

public class LongFieldDeserializer extends FieldDeserializer {

    private final ObjectDeserializer fieldValueDeserilizer;

    public LongFieldDeserializer(ParserConfig mapping, Class<?> clazz, FieldInfo fieldInfo){
        super(clazz, fieldInfo);

        fieldValueDeserilizer = mapping.getDeserializer(fieldInfo);
    }

    @Override
    public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
        Long value;

        final JSONLexer lexer = parser.getLexer();
        if (lexer.token() == JSONToken.LITERAL_INT) {
            long val = lexer.longValue();
            lexer.nextToken(JSONToken.COMMA);
            if (object == null) {
                fieldValues.put(fieldInfo.getName(), val);
            } else {
                setValue(object, val);
            }
            return;
        } else if (lexer.token() == JSONToken.NULL) {
            value = null;
            lexer.nextToken(JSONToken.COMMA);

        } else {
            Object obj = parser.parse();

            value = TypeUtils.castToLong(obj);
        }

        if (value == null && getFieldClass() == long.class) {
            // skip
            return;
        }

        if (object == null) {
            fieldValues.put(fieldInfo.getName(), value);
        } else {
            setValue(object, value);
        }
    }

    public int getFastMatchToken() {
        return fieldValueDeserilizer.getFastMatchToken();
    }
}

按照这个模子,很容易的就出来了我们的两个类。

public class DoubleFieldDeserializer extends FieldDeserializer {

    private final ObjectDeserializer fieldValueDeserilizer;

    public DoubleFieldDeserializer(ParserConfig mapping, Class<?> clazz, FieldInfo fieldInfo) {
        super(clazz, fieldInfo);

        fieldValueDeserilizer = mapping.getDeserializer(fieldInfo);
    }

    @Override
    public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
        Double value;

        final JSONLexer lexer = parser.getLexer();
        if (lexer.token() == JSONToken.LITERAL_INT) {
            long val = lexer.longValue();
            lexer.nextToken(JSONToken.COMMA);
            if (object == null) {
                fieldValues.put(fieldInfo.getName(), val);
            } else {
                setValue(object, val);
            }
            return;
        } else if (lexer.token() == JSONToken.NULL) {
            value = null;
            lexer.nextToken(JSONToken.COMMA);

        } else {
            Object obj = parser.parse();

            value = TypeUtils.castToDouble(obj);
        }

        if (value == null && getFieldClass() == double.class) {
            // skip
            return;
        }

        if (object == null) {
            fieldValues.put(fieldInfo.getName(), value);
        } else {
            setValue(object, value);
        }
    }

    @Override
    public int getFastMatchToken() {
        return fieldValueDeserilizer.getFastMatchToken();
    }
}
public class FloatFieldDeserializer extends FieldDeserializer {

    private final ObjectDeserializer fieldValueDeserilizer;

    public FloatFieldDeserializer(ParserConfig mapping, Class<?> clazz, FieldInfo fieldInfo) {
        super(clazz, fieldInfo);

        fieldValueDeserilizer = mapping.getDeserializer(fieldInfo);
    }

    @Override
    public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
        Float value;

        final JSONLexer lexer = parser.getLexer();
        if (lexer.token() == JSONToken.LITERAL_INT) {
            long val = lexer.longValue();
            lexer.nextToken(JSONToken.COMMA);
            if (object == null) {
                fieldValues.put(fieldInfo.getName(), val);
            } else {
                setValue(object, val);
            }
            return;
        } else if (lexer.token() == JSONToken.NULL) {
            value = null;
            lexer.nextToken(JSONToken.COMMA);

        } else {
            Object obj = parser.parse();

            value = TypeUtils.castToFloat(obj);
        }

        if (value == null && getFieldClass() == float.class) {
            // skip
            return;
        }

        if (object == null) {
            fieldValues.put(fieldInfo.getName(), value);
        } else {
            setValue(object, value);
        }
    }

    @Override
    public int getFastMatchToken() {
        return fieldValueDeserilizer.getFastMatchToken();
    }

万事具备,只欠东风,那么我们如何将我们的PatchParserConfig注入到解析的过程中去呢。我们使用android studio的快捷键command+左键一直跟进源码,发现JsonUtil工具类中的两个fromJson方法,最终会调用下面的两个方法

public static final <T> T parseObject(String input, Type clazz, ParserConfig config, ParseProcess processor,
                                          int featureValues, Feature... features) {
        if (input == null) {
            return null;
        }

        for (Feature featrue : features) {
            featureValues = Feature.config(featureValues, featrue, true);
        }

        DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);

        if (processor instanceof ExtraTypeProvider) {
            parser.getExtraTypeProviders().add((ExtraTypeProvider) processor);
        }

        if (processor instanceof ExtraProcessor) {
            parser.getExtraProcessors().add((ExtraProcessor) processor);
        }

        T value = (T) parser.parseObject(clazz);

        parser.handleResovleTask(value);

        parser.close();

        return (T) value;
    }

而该函数中新建的DefaultJSONParser对象内部维持了ParserConfig的一个引用,我们可以通过setter方法修改该引用为我们自己的的PatchParserConfig,于是一切都变得如此简单。

 private static final ParserConfig PARSER_CONFIG = new PatchParserConfig();
 private static final <T> T parseObject(String input, Type clazz, ParserConfig config, ParseProcess processor,
                                           int featureValues, Feature... features) {
        if (input == null) {
            return null;
        }

        for (Feature featrue : features) {
            featureValues = Feature.config(featureValues, featrue, true);
        }

        DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);
        parser.setConfig(PARSER_CONFIG);
        if (processor instanceof ExtraTypeProvider) {
            parser.getExtraTypeProviders().add((ExtraTypeProvider) processor);
        }

        if (processor instanceof ExtraProcessor) {
            parser.getExtraProcessors().add((ExtraProcessor) processor);
        }

        T value = (T) parser.parseObject(clazz);

        parser.handleResovleTask(value);

        parser.close();

        return (T) value;
    }

为了更加方便使用,我们在这个方法基础上再分装两个类似JSON类中的参数少的方法。

private static final <T> T parseObject(String text, Class<T> clazz) {
        return (T) parseObject(text, (Type) clazz, PatchParserConfig.getGlobalInstance(),null,JSON.DEFAULT_PARSER_FEATURE, new Feature[0]);
    }

    private static final <T> T parseObject(String text, TypeReference<T> type, Feature... features) {
        return (T) parseObject(text, type.getType(), PatchParserConfig.getGlobalInstance(), null, JSON.DEFAULT_PARSER_FEATURE, features);
    }

接着改造我们原来JsonUtil类中的fromJson的两个方法,将其调用执行自己内部的静态函数parserObject,如下。

  public static <T> T fromJson(String json, Class<T> clazz) {

        try {
            return clazz.equals(String.class) ? (T) json : parseObject(json, clazz);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    public static <T> T fromJson(String json, TypeReference<?> typeReference) {

        try {
            return (T) (typeReference.getType().equals(String.class) ? json
                    : parseObject(json, typeReference));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

最终,暴露给客户端的就是这两个方法。我们一个狸猫换太子,一下子就对服务器返回的空字符串进行了兼容。下面测试一下能否正常反序列化。输出结果如下

Bean{doubleParam=0.0, floatParam=2.76, extra=Extra{doubleParam=12.23, floatParam=0.0}}

异常消失了,世界都安静了。

时间: 12-25

Android FastJson与不规范JSON引发的血案的相关文章

一个无锁消息队列引发的血案(六)——RingQueue(中) 休眠的艺术 [续]

目录 (一)起因 (二)混合自旋锁 (三)q3.h 与 RingBuffer (四)RingQueue(上) 自旋锁 (五)RingQueue(中) 休眠的艺术 (六)RingQueue(中) 休眠的艺术 [续] 开篇 这是第五篇的后续,这部分的内容同时会更新和添加在 第五篇:RingQueue(中) 休眠的艺术 一文的末尾. 归纳 紧接上一篇的末尾,我们把 Windows 和 Linux 下的休眠策略归纳总结一下,如下图: 我们可以看到,Linux 下的 sched_yield() 虽然包括了

一个无锁消息队列引发的血案:怎样做一个真正的程序员?(二)——月:自旋锁

前续 一个无锁消息队列引发的血案:怎样做一个真正的程序员?(一)——地:起因 一个无锁消息队列引发的血案:怎样做一个真正的程序员?(二)——月:自旋锁 平行时空 在复制好上面那一行我就先停下来了,算是先占了个位置,虽然我知道大概要怎么写,不过感觉还是很乱. 我突然想到,既然那么纠结,那么混乱,那么不知所措,我们不如换个视角.记得高中时看过的为数不多的长篇小说<穆斯林的葬礼>,作者是:霍达(女),故事描写了两个发生在不同时代.有着不同的内容却又交错扭结的爱情悲剧,一个是“玉”的故事,一个是“月”

【转】Unity上同时兼容Android和IOS的JSON库

转自卡神博客Unity上同时兼容Android和IOS的JSON库 虽然说JSON解析很常见,而且也经常看见大家讨论怎么解析.但是还是很多人经常出现各种问题.这篇文章就一次性帮你解决JSON解析的问题. 本篇文章使用JSON解析在真实项目中使用,同时兼容PC.android和IOS.没啥好说,一个C#写的解析JSON的工具类,在unity中能正常解析. 不太好用的JSON解析库:LitJson在IOS上不稳定,有时正常,有时不正常..MiniJson据说支持不完整,没测试. 本文给出的JSON解

fastJson顺序遍历JSON字段

fastJson在把json格式的字符串转换成JSONObject的时候,使用的是HashMap,所以排序规则是根据HASH值排序的,如果想要按照字符串顺序遍历JSON属性,需要在转换的时候指定使用LinkedHashMap代替HashMap. public static void main(String[] args) { String jsonStr = "{\"size\":\"7.5\",\"width\":\"M (

Qt on Android: http下载与Json解析

百度提供有查询 ip 归属地的开放接口,当你在搜索框中输入一个 ip 地址进行搜索,就会打开由 ip138 提供的百度框应用,你可以在框内直接输入 ip 地址查询.我查看了页面请求,提取出查询 ip 归属地的接口,据此使用 Qt 写了个简单的 ip 归属地查询应用.可以在电脑和 Android 手机上运行.这里使用了百度 API ,特此声明,仅可作为演示使用,不能用作商业目的. 版权所有 foruok,转载请注明出处( http://blog.csdn.net/foruok ). 这个例子会用到

openstack运维实战系列(十三)之glance更改路径引发的&quot;血案&quot;

1. 背景说明 glance在openstack中负责镜像相关的服务,支持将运行的虚拟机转换为快照,镜像和快照都存储在glance中,glance的后端支持多种存储方式,包括本地的文件系统,http,glusterfs,ceph,swift等等. 默认情况下,glance采用本地文件系统的方式存储image,存储的路径为/var/lib/glance/images,随着时间的推移,当镜像越来越多的时候,根目录的空间将会越来越大,所以对于glance的路径来说,需要提前做好规划和准备,如划分一个单

php如何解析IOS/Android上传的Json消息

使用yii框架php服务器接收IOS/Android上传的Json消息时,$_POST. Yii::app()->request->getPost()结果都将为null. 使用file_get_contents("php://input")才能够正常获取. "php://input"可以访问请求的原始数据,并且带给内存的压力更小. 例子如下: class appController extends Controller { public function

一个二级菜单引发的血案

近期发现自己css不是很好,于是又看了一遍<css权威指南>.总感觉自己抓不到重点.弃疗中...于是看看其他书.然后学妹跟我说她的二级菜单写得很乱.当时我心里就在想二级菜单,有何难?自认为10分钟能搞定.跟她要效果图并很自大的说了句“等会儿,我写个简单的”.于是血案由此引发... 二级菜单要实现的原效果图是: (如发现雷同,不是巧合,是我从别的网页上截屏下来的 ~_~).既然说了简单,肯定效果没这么精美.但是至少基本效果和原理要实现. 10分钟过去了....15分钟过去了....这个“等会儿”

Android数据格式解析对象JSON用法

1.JSON概念: 一种轻量级的数据交换格式,具有良好的可读和便于快速编写的特性,从而可以在不同平台间进行数据交换.JSON采用兼容性很高的文本格式,同时也具备类似于C语言体系的行为. JSON可以将Java对象转成json格式的字符串,可以将json字符串转换成Java.比XML更轻量级,Json使用起来比较轻便和简单.JSON数据格式,在Android中被广泛运用于客户端和服务器通信,在网络数据传输与解析时非常方便. 2.环境配置 http://code.google.com/p/googl