fastjson 反序列化源码解析

fastjson从0.X版本到最新版本,由于其特殊的编码(即特别编码优化),让人对其实现思路很难处理,加上没有注释,因此在学习了解时也很困难。因此,本篇即从基本概念入手,忽略其一些特殊处理点(对实际结果无意义),了解其反序列化实现思路。
本文基于fastjson版本 1.2.4

1 基本概念

  • token-词法标记      用于标识当前在解析过程中解析到的对象的一个标记,具体的值参考 JSONToken。比如 {,即表示当前正在解析的是一个类似map或对象的格式,而},则表示当前对象已经到底了。
  • ch-当前字符    用于表示当前已经读取到的字符是什么,如 abc,当位置为1时,则当前字符为 b
  • bp-解析字符位置    用于表示当前字符所位于原始字符串中的哪一个位置,与ch是相对应的,它始终表示最新的一个位置,如果需要记录一些历史位置。如字符串起始位置,数字起始位置等,则需要使用其它标记,如np。
  • sbuf-字符缓冲    在解析字符串时的特殊存储区域,主要是用于解析转义字符时的临时存储区。即如果原字符串为 a\\t,则实际解析的字符串应该为a\t,那么原字符串为3位长,解析之后为2位长。即需要另行存储。字符缓冲区如名所示,为一个字符数组,需要需要单独的定义来存储长度信息,如使用sp。
  • sp-字符缓冲区位置    这个用于表示在字符缓冲区之间记录当前字符串(或数字串)等的长度信息,同时也等同于当前的一个位置(如果坐标从0开始)。
  • np-数字解析位置    用于实际表示在解析到常量信息时起始点的标记位置。通过np + sp,即计算得出相应的区间值了。

2    解析规则

2.1     定义规则

  • 字符串    以"开头,并且以"结束,在中间可以存在以\为转义符,后面接"的情况,如 "\"",认为是正确的。但 "\""",认为是错误的,其解析时在倒数第2个双引号时即结尾了。
  • 数字整形     以0-9开始,并且以连续数字,末尾可以s,b,f,d,l等
  • 数字小数    以0-9开始,默认为f,D或者,中间存在小数点
  • bool值    必须是 true 或 false ,全小写
  • 数组集合    以[ 开头,以]结尾,中间以任意,分隔的信息
  • set    以Set开头的,后面接数组集合的信息
  • treeSet    以TreeSet开头的,后面接数组集合的信息
  • 对象map    以{开始,以}结尾,中间按key,value集合的信息.
  • null值    必须为null
  • 字段    字符串,或单引号字符串,或无引号字符串,无引号时,必须为a-zA-Z_开头,以a-zA-Z0-9_结束.

以下附一个antlr解析json的词法表:https://github.com/antlr/grammars-v4/blob/master/json/JSON.g4

json:   object |   array;
object:   '{' pair (',' pair)* '}'    |   '{' '}' // empty object    ;
pair:   STRING ':' value ;
array    :   '[' value (',' value)* ']'    |   '[' ']' // empty array    ;
value    :   STRING    |   NUMBER    |   object  // recursion    |   array   // recursion    |   'true'  // keywords    |   'false'    |   'null'    ;

STRING :  '"' (ESC | ~["\\])* '"' ;
fragment ESC :   '\\' (["\\/bfnrt] | UNICODE) ;
fragment UNICODE : 'u' HEX HEX HEX HEX ;
fragment HEX : [0-9a-fA-F] ;
NUMBER    :   '-'? INT '.' [0-9]+ EXP? // 1.35, 1.35E-9, 0.3, -4.5    |   '-'? INT EXP             // 1e10 -3e4    |   '-'? INT                 // -3, 45    ;
fragment INT :   '0' | [1-9] [0-9]* ; // no leading zeros
fragment EXP :   [Ee] [+\-]? INT ; // \- since - means "range" inside [...]
WS  :   [ \t\n\r]+ -> skip ;

标记json中只支持数组和对象型,但实际上,在反序列化时,value也认为是json的一部分。

2.2     词法规则

按照上面的定义,实际上就是贪婪的匹配规则,一旦满足一个匹配规则,那么这个匹配就要继续下去,直到当前规则不能完成时,同时在下一个规则之间,使用特定的分隔符作连接。

如 字符串 {a:"123",b:[1,2,3]},即按以下规则进行

  1. 对象开始:{
  2. 对象key(即字段):a
  3. 分隔符: :
  4. 对象value开始:
  5. 字符串开始: "
  6. 字符串:123
  7. 字符串结束: "
  8. 对象value:结束:
  9. 对象间分隔符:,
  10. 对象key: b
  11. 分隔符: :
  12. 对象value开始:
  13. 数组开始: [
  14. 数组值1数字开始: 1
  15. 数组值1数字结束: 1,
  16. 数组分隔符: ,
  17. 数组结束: ]
  18. 对象结束: }

2.3     词法解析

整个词法,即TOKEN流,是由类JSONLexerBase来负责完成的,其负责提供主要词法单元的解析和结果取值操作。相应方法对应关系如下所示

  • 数字    scanNumber     numberString     intValue     longValue     floatValue     doubleValue
  • 字符串    scanString     stringVal
  • NULL值    scanNULL,scanNullOrNew
  • Boolean值    scanTrue,scanFalse

3    语法解析

3.1     结束符判定

在词法解析上,fastjson根据第1个有效字符判定相应的类型,然后直到该类型结束之后,立即采用该类型。如以{开头,则一定返回object类型。对于类型 "{key:value} other" 这种非正确字符串,fastjson采用尾判断规则,即有效对象解析完毕之后,判定结束符必须已经到达字符串尾。如果Token值不是EOF,则表示字符串出现问题,而提示相错误的信息。

具体的判定,对应方法 DefaultJSONParser中的close()方法。

3.2     忽略类型

默认情况下,fastjson对于json类型分别采用 set,treeSet,jsonArray,jsonObject,int(包含int,long,biginteger),float(包含float,double,bigDecimal),string,null,true,false来进行解析。即除基本的value词法表示外,其它均使用通用类型来表示。即数组使用set和jsonArray,对象使用jsonObject。

3.2.1     数组解析

对应方法 Object parse(Object fieldName)

case LBRACKET:
    JSONArray array = new JSONArray();
    parseArray(array, fieldName);
    return array;

如上所示,语法规则 [ 开始,表示为数组,则定义jsonArray,然后将此引用传递给具体的解析数组的方法中,以进行处理。jsonArray可以理解为使用ArrayList封装的复合对象。上面的fieldName解析对象时标识相应的key值,这里默认为null。以下代码忽略非关键性处理

public final void parseArray(final Collection array, Object fieldName) {
.....
    final JSONLexer lexer = getLexer();
    //因为当前位置为[ 跳转到下一个词法单元处
    lexer.nextToken(JSONToken.LITERAL_STRING);

        for (int i = 0;; ++i) {
......
            Object value;
            switch (lexer.token()) {
                case LITERAL_INT:
//当前值为int,解析value值,并跳转到下一个标记处
                    value = lexer.integerValue();
//如当前字符串为 1,2 则解析完1之后,跳转到,处,以方便后面作判断,并跳转至2处
//这里的nextToken中的参数表示期望值,但实际上也不一定是该值,不过可以根据该值作一个进一步判定相应值。如这里期望,但也可以是一个 ],而表示解析结束
                    lexer.nextToken(JSONToken.COMMA);
                    break;
                case LITERAL_FLOAT:
......//解析小数
                case LITERAL_STRING:
......//解析字符串
                    break;
                case TRUE://解析true
                case FALSE://解析false
                case LBRACE:
//这里碰到一个{,表示数组中还内嵌有对象,则跳转到解析对象的地方
                    JSONObject object = new JSONObject(isEnabled(Feature.OrderedField));
                    value = parseObject(object, i);
                    break;
                case LBRACKET:
//这里碰到 [,则表示数组中还内嵌有数组,跳转到解析子数组的地方,这里的items值是新数组
                    Collection items = new JSONArray();
                    parseArray(items, i);
                    value = items;
                    break;
                case NULL://解析null值
                case UNDEFINED://js中的undefiend也认为是null值
                case RBRACKET:
//这里碰到 ],则表示当前数组已经解析完毕,可以正常的return了。其它地方都是继续循环处理,即只有在这里才能正常跳出循环
                    lexer.nextToken(JSONToken.COMMA);
                    return;
                case EOF:
//非正常跳出循环的地方,即字符串一下未匹配到] 就到末尾了
                    throw new JSONException("unclosed jsonArray");
                default:
//默认解析
                    value = parse();
                    break;
            }

//将上面解析到的对象添加到数组中
            array.add(value);

//这里因为前面在解析完之后,均往前解析了一位,即期望碰到 ,这里判断如果是 ,值 表示还有后续的value值
//则继续往前解析,如 a,b 则期望当前位置从,处解析到b处
            if (lexer.token() == JSONToken.COMMA) {
                lexer.nextToken(JSONToken.LITERAL_STRING);
                continue;
            }
        }

3.3     类型处理

上面的为不带类型处理,则fastjson不知道应该将返回类型设置为什么类型。如果调用方主动的提示处理,则采用另一种处理方式,则根据提示类型对array以及object作处理。实际上,带类型处理的话,则会使用到ObjectDeserializer来进行解析。不过这里又会重新调用 DefaultJSONParser来进行处理。如对于(数组)或集合,则会使用 parseArray(Type type, Collection array, Object fieldName) 通过传递回来的collection对象,将相应值并根据type进行解析,再放回集合中。并根据返回类型进行调整。如数组,则会使用jsonArray进行二次转换来得到最终结果。代码参考如下:

    public void parseArray(Type type, Collection array, Object fieldName) {
//这里根据type值获取具体类型的解析器
        ObjectDeserializer deserializer = null;
        if (int.class == type) {
            deserializer = IntegerCodec.instance;
            lexer.nextToken(JSONToken.LITERAL_INT);
        }......

            for (int i = 0;; ++i) {
......
                if (int.class == type) {
                    Object val = IntegerCodec.instance.deserialze(this, null, null);
                    array.add(val);
                }
......

//下一个元素解析
                if (lexer.token() == JSONToken.COMMA) {
                    lexer.nextToken(deserializer.getFastMatchToken());
                    continue;
                }
            }

可以看出,此处解析与通用解析逻辑基本一致,惟一不同的即是这里使用了针对类型的各种deserializer解析器来完成针对类型的工作。

4    面向对象封装

在实际的使用场景,我们均会使用到如 parseObject(String text, Class<T> clazz) 来期望返回具体的类型,这里实际上就会调用到了不同的类型反序列化器了。fastjson根据这里的类型,调用相应的序列化对象来完成不同的对象解析工作。

4.1     类型映射

而序列化器的工作也并不是进行具体的语法解析,而是提供相应的类型信息,以期望jsonParser进行正常的解析工作。即具体的解析工作仍是由jsonParser来完成,ObjectDeserializer只不过提供一些上下文信息,以及对流程进行控制。

从 ObjectDeserializer的继承上可以看出,存在很多不同的反序列化器。同时,对于fastjson内置的反序列化器,采用了 ParserConfig.derializers来进行内部存储。因为默认情况下,会使用parseConfig的单例对象,因此这里的存储是全局共享的(实际上也没有关系)

4.1.1     数字解析
对应类为IntegerCodec

    public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
        final JSONLexer lexer = parser.getLexer();

        Integer intObj;
        if (lexer.token() == JSONToken.LITERAL_INT) {
//如果token匹配到数字,而直接通过integer.parse强转
            int val = lexer.intValue();
            lexer.nextToken(JSONToken.COMMA);
            intObj = Integer.valueOf(val);
        } else if (lexer.token() == JSONToken.LITERAL_FLOAT) {
//匹配到小数,而这里需要整数,而截取掉
            BigDecimal decimalValue = lexer.decimalValue();
            lexer.nextToken(JSONToken.COMMA);
            intObj = Integer.valueOf(decimalValue.intValue());
        } else {
//其它类型,采用类型转换强制转换,如匹配到字符串,也是可以转换为 整数的
            Object value = parser.parse();

            intObj = TypeUtils.castToInt(value);
        }
        
//单独处理,使用IntegerCodec同时支持 int和 atomicInteger两种,算是偷懒吧
        if (clazz == AtomicInteger.class) {
            return (T) new AtomicInteger(intObj.intValue());
        }
        
        return (T) intObj;
    }

4.1.2     数组(集合)解析

对应类CollectionDeserializer 数组对应为ArrayDeserializer

    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {

//根据类型获取不同的集合实现
        Class<?> rawClass = getRawClass(type);

//以下即根据不同的接口类 作不同的实现。因此在应用中,经常使用接口来标识不同的集合类
        Collection list;
        if (rawClass == AbstractCollection.class) {//ArrayList
        } else if (rawClass.isAssignableFrom(HashSet.class)) {//HashSet
        } else if (rawClass.isAssignableFrom(LinkedHashSet.class)) {//LinkedHashSet
        } else if (rawClass.isAssignableFrom(TreeSet.class)) {//TreeSet
        } else if (rawClass.isAssignableFrom(ArrayList.class)) {//List
        } else if (rawClass.isAssignableFrom(EnumSet.class)) {//EnumSet
        } else {//默认情况下,直接实例化
                list = (Collection) rawClass.newInstance();
        }

//这里尝试获取泛型实例信息,如 List<String> 则获取string,即期望集合中每一项值均是 字符串
        Type itemType;
        if (type instanceof ParameterizedType) {
            itemType = ((ParameterizedType) type).getActualTypeArguments()[0];
        } else {
            itemType = Object.class;
        }
//调用3.3中的不同类型解析公式处理
        parser.parseArray(itemType, list, fieldName);

        return (T) list;
    }

4.2     javaBean封装解析

在实际解析过程中 javaBean与map的解析规则基本上一致。不过在map中的key值是任意的,而在javaBean中key值是固定的。即javaBean中可以控制在反序列化时哪些key是可接收的,哪些是不可接收的。

同时,javaBean通过配置项JsonField,可以重新配置字段的别名,从而映射到其它的key上。在map中, 均不存在相应的处理.

javaBean的过程可以理解为,先创建对象,然后每于{}中的每一项,先匹配key值,然后根据key值查找到相应的字段信息,根据不同的字段再解析该字段值。即语法表中的
pair:   STRING ':' value ;

具体的解析代码如下所示(忽略非关键信息)

    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object) {
//简化判断,即针对原生json对象,使用类似map的解析规则
        if (type == JSON.class || type == JSONObject.class) {
            return (T) parser.parse();
        }
        
//如果直接是一个null,则直接返回null即可,表示该对象不存在
        if (lexer.token() == JSONToken.NULL) {
            lexer.nextToken(JSONToken.COMMA);
            return null;
        }

            Map<String, Object> fieldValues = null;

//预处理,如果直接为 {},则表示是空对象(不是null,直接返回
            if (lexer.token() == JSONToken.RBRACE) {
                lexer.nextToken(JSONToken.COMMA);
                    object = createInstance(parser, type);
                return (T) object;
            }
......

            for (;;) {
//查找字段值
                String key = lexer.scanSymbol(parser.getSymbolTable());
......
//这里因为找到了具体的字段,则根据字段进行解析该字段信息
                boolean match = parseField(parser, key, object, type, fieldValues);
//这里碰到了},表示对象已经解析结束,就不再处理了                
                if (lexer.token() == JSONToken.RBRACE) {
                    lexer.nextToken(JSONToken.COMMA);
                    break;
                }
            }

            return (T) object;

至于在parseField中,可以理解为fastjson对每一个字段产生了一个fieldDeserializer,然后使用fieldDeserializer单独进行数据解析。其实这里也可以使用通用的objectDeserializer,只不过这里将字段的设置值结合在一起来进行处理了。即fieldDeserializer同时持有field信息,也同时解析相应的字符串。具体的可以参考类 FieldDeserializer以及相关子类.

6    扩展点

  • JsonField    用于描述字段的序列信息及反序列信息,如重新设定name值,是否需要反序列化等.
  • JsonType    用于控制指定的类进行反序列化时的信息,如使用其它类作为映射信息.
  • JsonCreator    用于控制在反序列化时初始化object时,采用哪一个构造函数或者工厂方法
  • ExtraTypeProvider    用于控制在javaBean序列化时,字段的类型信息重新设定。比如 字段类型为Object,可以通过这个接口重新设定它的具体类型.
  • ExtraProcessor    用于控制在序列化及反序列化时,如果碰到不同的解析的字段时,可以通过此接口重新 进行处理。如 字段a,在{"b":"abc"},可以通过这个接口重新将字段b所对应的值映射到字段a上。
  • ObjectDeserializer    最后可以自已定义反序列化器,通过ParseConfig.putDeserializer 来添加相应的反序列化器

7    总结

其实在整个过程中,最核心的部分在于词法分析和语法解析.另外由于业务的复杂性以及在编码时的代码优化,fastjson在一些具体实现时,有一些额外的处理,导致逻辑上不是很懂。但是,理解了词法和语法,其它的看起来就不是太难了。

转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201508160001.html

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注