天天看點

使用ExpressionTree實作JSON解析器

  今年的春節與往年不同,對每個人來說都是刻骨銘心的。突入其來的新型冠狀病毒使大家過上了“夢想”中的生活,如今這樣的生活令人一點都不踏實,隻有不停的學習才能讓人安心。于是我把年前弄了一點的JSON解析器實作了一下,序列化/反序列化對象轉換這部分主要用到了ExpressionTree來實作,然後寫了這篇文章來介紹這個項目(檢視源碼)。

先展示一下使用方法:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器

1     public class Student
 2     {
 3         public int Id { get; set; }
 4         public string Name { get; set; }
 5         public Sex Sex { get; set; }
 6         public DateTime? Birthday { get; set; }
 7         public string Address { get; set; }
 8     }
 9 
10     public enum Sex
11     {
12         Unkown,Male,Female,
13     }      

Student

json反序列化成Student:

var json = "{\"id\":100,\"Name\":\"張三\",\"Sex\":1,\"Birthday\":\"2000-10-10\"}";
var student = JsonParse.To<Student>(json);        

Student序列化為json:

var student = new Student
            {
                Id = 111,
                Name = "testName",
                Sex = Sex.Unkown,
                Address = "北京市海澱區",
                Birthday = DateTime.Now
            };
            var json = JsonParse.ToJson(student);
            //{"Id":111,"Name":"testName","Sex":"Unkown","Birthday":"2020-02-15 17:43:31","Address":"北京市海澱區"}
            var option = new JsonOption
            {
                WriteEnumValue = true, //序列化時使用枚舉值
                DateTimeFormat = "yyyy-MM-dd" //指定datetime格式
            };
            var json2 = JsonParse.ToJson(student, option);
            //{"Id":111,"Name":"testName","Sex":0,"Birthday":"2020-02-15","Address":"北京市海澱區"}      

json反序列化List,Ienumerable,Array:

var json = "[{\"id\":100,\"Name\":\"張三\",\"Sex\":1,\"Birthday\":\"2000-10-10\"},{\"id\":101,\"Name\":\"李四\",\"Sex\":\"female\",\"Birthday\":null,\"Address\":\"\"}]";
  var list = JsonParse.To<List<Student>>(json);
  var list2 = JsonParse.To<IEnumerable<Student>>(json);
  var arr = JsonParse.To<Student[]>(json);              

List<Stuednt> 轉換為json

var list = new List<Student>
            {
                new Student {Id=123,Name="username1",Sex=Sex.Male,Birthday = new DateTime(1980,1,1) },
                new Student {Id=125,Name="username2",Sex=Sex.Female},
            };
            var json1 = JsonParse.ToJson(list, true); //使用縮進格式,預設是壓縮的json
            /*
            [
                {
                    "Id":123,
                    "Name":"username1",
                    "Sex":"Male",
                    "Birthday":"1980-01-01 00:00:00",
                    "Address":null
                },
                {
                    "Id":125,
                    "Name":"username2",
                    "Sex":"Female",
                    "Birthday":null,
                    "Address":null
                }
            ] 
            */
            var option = new JsonOption
            {
                Indented = true,    //縮進格式
                DateTimeFormat = "yyyy-MM-dd",
                IgnoreNullValue = true //忽略null輸出
            };
            var json2 = JsonParse.ToJson(list, option);
            /*
               [
                    {
                        "Id":123,
                        "Name":"username1",
                        "Sex":"Male",
                        "Birthday":"1980-01-01"
                    },
                    {
                        "Id":125,
                        "Name":"username2",
                        "Sex":"Female"
                    }
                ]
             */      

json轉為Dictironary:

//Json to Dictionary
var json = "{\"确診病例\":66580,\"疑似病例\":8969,\"治愈病例\":8286,\"死亡病例\":1524}";
var dic = JsonParse.To<Dictionary<string, int>>(json);
var dic2 = JsonParse.To<IDictionary<string, int>>(json);      

 JsonParse提供了一些可以重載的對象序列化/反序列化的靜态方法,内部實際是調用JsonSerializer去完成的,更複雜的功能也是需要利用JsonSerializer來實作的,這個不是重點就不去介紹了。

  對于JSON的解析主要包含兩個功能:序列化和反序列化,序列化是将對象轉換為JSON字元串,反序列化是将JSON字元串轉換為指定的對象。本項目涉及到的幾個核心對象有JsonReader、JsonWriter、 ITypeConverter、IConverterCreator等,下面一一介紹。

1、JsonReader json讀取器

  JsonReader可以簡單的了解為一個json字元串的掃描器,按照json文法規則進行掃描,每次掃描取出一個JsonTokenType及其對應的值,JsonTokenType枚舉定義:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1   public enum JsonTokenType : byte
 2     {
 3         None,    
 4         StartObject,  //{
 5         EndObject,    //}   
 6         StartArray,   //[
 7         EndArray,     //]
 8         PropertyName, //{辨別後雙引号包圍的字元串或{内逗号後雙引号包圍的字元串 解析為PropertyName
 9         String,    //除PropertyName外雙引号包圍的字元串
10         Number,    //沒有引号包圍的數字  
11         True,      //true
12         False,     //false
13         Null,      //null
14         Comment    //注釋
15     }      

View Code

字元串掃描方法 Read() :

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1         public bool Read()
 2         {
 3             switch (_state)
 4             {
 5                 case ReadState.Start: _line = _position = 1; return ReadToken();
 6                 case ReadState.StartObject: return ReadProperty();
 7                 case ReadState.Property:
 8                 case ReadState.StartArray: return ReadToken();
 9                 case ReadState.EndObject:
10                 case ReadState.EndArray:
11                 case ReadState.Comma:
12                 case ReadState.Value: return ReadNextToken();
13                 case ReadState.End: return ValidateEndToken();
14                 default: throw new JsonException($"非法字元{_currentChar}", _line, _position);
15             }
16         }      

  從Read方法可以看出JsonReader内部維持了一個ReadState狀态機,每次調用根據上一個ReadState來進行下一個token的解析,這樣既驅動了内部方法分支跳轉,同時又比較容易的對json格式進行校驗,例如:遇到 {(StartObject) 下一個有效字元(空白字元除外)隻能是 “(PropertyName)或 }(EndObject)之一,是以當ReadState=StartObject時應該去執行ReadProperty()方法,而在ReadProperty()方法裡隻需要對 ” 和 } 兩個字元做正确的響應,出現其他字元都說明這個json文檔格式不正确,抛異常就行了,是以ReadProperty()方法的核心代碼如下所示:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1 private bool ReadProperty()
 2 {
 3        var value =  MoveNext(true);
 4        switch (value)
 5        {
 6             case '"':
 7                 //讀取propertyName值
 8                 return true;
 9             case '}':
10                 //readState狀态值切換
11                 return true;
12             default: throw new JsonException($"非法字元{value }", _line, _position);
13         }
14 }
15           

....等等其他方法的跳轉和格式的校驗都是采用類似方法處理的。

  token的校驗有一個比較麻煩的地方就是容器(JsonObject和JsonArray)嵌套後符号的閉合是否正确,即{與},[與]必須成對出現,比如: [ { } } ]這個錯誤的json字元串,如果僅僅利用上一個token來驗證下一個token是否合法,是無法判斷出這個json是不合法的, 這時Stack後進先出的特性就非常适合這個場景了,借助Stack我們可以這樣驗證這個json:遇到第一個[,進行壓棧操作;第二個{,繼續壓棧;第三個},出棧操作,對出棧的值進行判斷與目前值是否能閉合,出棧值是{,剛好與}是成對的,那麼第三個字元是合法的,此時棧頂值是[;第四個字元},出棧操作,出棧的值是[,與}無法成對,值非法,驗證結束。

  JsonReader的核心功能是對json文本的拆解與校驗,核心方法就是Read(),調用Read()方法會有3中情況存在:1.傳回true,正确讀取到一個JsonTokenType且文檔未讀完  2.傳回false,正确讀取到一個JsonTokenType且文檔已全部讀取完畢 3.出現異常,json格式不正确或不滿足配置要求。上層的反序列化功能都是依賴JsonReader來完成的,使用JsonReader讀完一個json後得到的是一組的JsonTokenType以及對應的值,至于這些tokentype之間所包含的層級關系會由後面的ITypeConverter或JsonToken等對象進行處理。

2、JosnWriter json寫入器

  JosnWriter和JsonReader的功能則相反,是将資料按照json規範輸出為json字元串,序列化功能類最終都是交給JosnWriter來完成的。調用JsonWriter的寫入方法每次會寫入一個JsonTokenType值,當然寫的時候也需要校驗值是否合法,校驗邏輯與JsonReader的校驗差不多,功能相對簡單就不去介紹了,有興趣的同學可以直接看代碼,代碼位址在文檔末尾。

3、(反)序列化接口ITypeConverter

 主要類之間的引用關系圖:

使用ExpressionTree實作JSON解析器

  

  ITypeConverter接口是整個對象序列化/反序列化過程的核心,ITypeConverter的職責是依托于JsonReader,JsonWriter來實作特定對象類型的(反)序列化,但是光有ITypeConverter還不夠,因為是特定對象的(反)序列化器,一個ITypeConverter實作類隻能解析一個或一類對象,解析一個對象會用到很多個ITypeConverter,對于外部調用者來說根本不知道什麼的時候使用哪個ITypeConverter,這個工作就交給了IConverterCreator工廠來完成,看下IConverterCreator的定義:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1 public interface IConverterCreator
2     {
3         bool CanConvert(Type type);
4 
5         ITypeConverter Create(Type type);
6     }      

使用這個工廠建立ITypeConverter前需要調用CanConvert方法來判斷給定的Type是否支援,當傳回true時就可以去建立對應的TypeConverter,不然建立出來了也不能正常工作,這樣就需要有一堆IConverterCreator的候選項來供調用者查找,然後去周遊這些候選項調用CanConvert方法,當周遊到某個候選項傳回true時,就可以建立ITypeConverter開始幹活了,基于此抽象了一個TypeConverterProvider類:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1  public abstract class TypeConverterProvider
 2     {
 3         public abstract IReadOnlyCollection<IConverterCreator> AllConverterFactories();
 4 
 5         public abstract void AddConverterFactory(IConverterCreator converter);
 6 
 7         public virtual ITypeConverter Build(Type type)
 8         {
 9             ITypeConverter convert = null;
10             foreach (var creator in AllConverterFactories())
11             {
12                 if (creator.CanConvert(type))
13                 {
14                     convert = creator.Create(type);
15                     break;
16                 }
17             }
18             if (convert == null) throw new JsonException($"建立{type}的{nameof(ITypeConverter)}失敗,不支援的類型");
19             return convert;
20         }
21     }      

為了能夠擴充使用自定義實作的IConverterCreator,提供了一個AddConverterFactory方法,可以從外部添加自定義的IConverterCreator。Build方法的預設實作就是周遊AllConverterFactories,然後判斷是否能建立ITypeConverter,隻要符合條件就調用IConverterCreator的Create方法來建立ITypeConverter傳回,整個工廠生成器實作閉合,理論上隻要AllConverterFactories裡面的IConverterCreator足夠多或者足夠強大,能夠轉換所有類型的Type,那麼這個工廠生成器就可以利用IConverterCreator建立ITypeConverter來實作任意類型的(反)序列化工作了。

4、用ExpressionTree對ITypeConverter的幾個實作  

 4.1 TypeConverterBase

  利用表達式樹生成委托的功能,然後将委托緩存下來,執行性能可以和靜态編寫的代碼相當。TypeConverterBase提取了一個公共屬性Func<object> CreateInstance,目的是為反序列化建立Type的對象是調用,委托的是使用表達式樹編譯生成:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1  protected virtual Func<object> BuildCreateInstanceMethod(Type type)
 2         {
 3             NewExpression newExp;
 4             //優先擷取無參構造函數
 5             var constructor = type.GetConstructor(Array.Empty<Type>());
 6             if (constructor != null)
 7                 newExp = Expression.New(type);
 8             else
 9             {
10                 //查找參數最少的一個構造函數
11                 constructor = type.GetConstructors().OrderBy(t => t.GetParameters().Length).FirstOrDefault();
12                 var parameters = constructor.GetParameters();
13                 List<Expression> parametExps = new List<Expression>();
14                 foreach (var para in parameters)
15                 {
16                     //有參構造函數使用預設值填充
17                     var defaultValue = GetDefaultValue(para.ParameterType);
18                     ConstantExpression constant = Expression.Constant(defaultValue);
19                     var paraValueExp = Expression.Convert(constant, para.ParameterType);
20                     parametExps.Add(paraValueExp);
21                 }
22                 newExp = Expression.New(constructor, parametExps);
23             }
24             Expression<Func<object>> expression = Expression.Lambda<Func<object>>(newExp);
25             return expression.Compile();
26         }      

這個方法首先判斷該類型是否有無參的構造函數,如果有就直接通過Expression.New(type)去構造,沒有的話去查找參數最少的一個構造函數來構造,構造帶參數構造函數的時候是需要傳遞這些參數的,預設實作是直接傳遞目前參數類型的預設值,當然也是可以通過配置等方式來指定參數資料值的。擷取一個type預設值的表達式Expression.Default(type),如果類型是int,就相當于default(int),如果類型是string,就相當于default(string)等等。然後使用常量表達式Expression.Constant(defaultValue)轉換成Expression,将轉換的結果添加到List<Expression>中,再使用構造函數表達式的重載方法newExp= Expression.New(constructor, parametExps),轉換成lambad表達式Expression.Lambda<Func<object>>(newExp),就可以調用Compile方法生成委托了。

  有了Func<object> CreateInstance這個委托方法,執行個體化對象就隻需要執行委托就行了,也不用反射建立去對象了。

  TypeConverterBase的具體實作類大體歸為3類,處理JsonObject類型的解析器:ObjectConverter、DictionaryConverter,處理JsonArray類型的解析器:EnumberableConverter(具體實作有ListConverter,ArrayConverter...); 處理Json值類型(JsonString,JsonNumber,JsonBoolean,JsonNull)的解析器:ValueConverter。每個解析器都是針對各自類型特點來完成json(反)序列化的。

 4.2 對象解析器 ObjectConverter

  為了能使對象中的屬性/字段能與JsonObject中的Property進行互相轉化,我們定義了2個委托屬性:Func<object, object> GetValue,設定屬性/字段值Action<object, object> SetValue。參數的定義都是使用object類型的,目的是為了保證方法的通用性。GetValue是擷取屬性/字段值的委托方法,第一個入參object是目前類的執行個體對象,傳回的object是對應屬性/字段的值。看下GetValue委托生成的代碼:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1         protected virtual Func<object, object> BuildGetValueMethod()
2         {
3             var instanceExp = Expression.Parameter(typeof(object), "instance");
4             var instanceTypeExp = Expression.Convert(instanceExp, MemberInfo.DeclaringType);
5             var memberExp = Expression.PropertyOrField(instanceTypeExp, MemberInfo.Name);
6             var body = Expression.TypeAs(memberExp, typeof(object));
7             Expression<Func<object, object>> exp = Expression.Lambda<Func<object, object>>(body, instanceExp);
8             return exp.Compile();
9         }      

首先定義好方法的參數var instanceExp = Expression.Parameter(typeof(object), "instance"),入參是object類型的,使用的時候是需要轉換成其真實類型的,使用Expression.Convert(instanceExp, MemberInfo.DeclaringType),Expression.Convert是做類型轉換的(Expression.TypeAs也可以類型轉換,但轉換類型如果是值類型會報錯,隻能用于轉換為引用類型),然後再用Expression.PropertyOrField(instanceTypeExp, MemberInfo.Name),傳入執行個體與成員名稱就可以擷取到成員值了,這個GetValue方法的邏輯就相當于下面的僞代碼:

protected object GetValue(object obj)
        {
            var instance = (目标類型)obj;
            var value = instance.目标屬性/字段;
            return (object)value;
        }
      

再看看SetValue委托的生成邏輯:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1       protected virtual Action<object, object> BuildSetValueMethod()
 2         {
 3             var instanceExp = Expression.Parameter(typeof(object), "instance");
 4             var valueExp = Expression.Parameter(typeof(object), "memberValue");
 5 
 6             var instanceTypeExp = Expression.Convert(instanceExp, MemberInfo.DeclaringType);
 7             var memberExp = Expression.PropertyOrField(instanceTypeExp, MemberInfo.Name);
 8             //成員指派
 9             var body = Expression.Assign(memberExp, Expression.Convert(valueExp, MemberType));
10             Expression<Action<object, object>> exp = Expression.Lambda<Action<object, object>>(body, instanceExp, valueExp);
11             return exp.Compile();
12         }      

指派操作不需要有傳回值,第一個參數是執行個體對象,第二個參數是成員對象,都通過Expression.Parameter方法聲明,Expression.PropertyOrField是擷取屬性/字段的表達式相當于靜态代碼的instance.屬性/字段名 這樣的寫法,成員指派表達式:Expression.Assign(memberExp, Expression.Convert(valueExp, MemberType)),成員入參聲明的是object,同樣需要調用Expression.Convert(valueExp, MemberType) 來轉換成真實類型。然後使用Expression.Lambda的Compile方法就可以生成目标委托了。

  一個類裡會有多個屬性/字段,每個屬性/字段都需要對應各自的GetValue/SetValue, 我們将GetValue/SetValue委托的生成統一放在了MemberDefinition類中,一個MemberDefinition隻負責管理一個成員資訊(PropertyInfo或FieldInfo)的讀寫委托的生成,然後在ObjectConverter裡面維護了一個MemberDefinition清單public IEnumerable<MemberDefinition> MemberDefinitions 來映射目前類的多個屬性/字段,每次對成員指派或寫值時,隻需要找到對應的MemberDefinition,然後調用其GetValue/SetValue委托就可以了。

 4.3 字典類型解析器 DictionaryConverter

DictionaryConverter為了處理Dictionary<,>與JsonObject之間互轉換的,因為是泛型接口,鍵與值的類型需要用兩個屬性來儲存

public Type KeyType { get; protected set; }

public Type ValueType { get; protected set; }
      

 這兩個Type類型的屬性是為了指派/寫值時類型轉換用的。 與對象成員指派的方法不一樣,字典鍵值的讀寫可以通過索引器來完成,字典指派委托:Action<object, object, object>,第一個參數是字典執行個體,第二個參數是key的值,第三個參數是value的值,執行這個委托就等于調用這句代碼:dic[key]=value; 來看一下表達式生成這個委托的代碼:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
protected virtual Action<object, object, object> BuildSetKeyValueMethod(Type type)
        {
            var objExp = Expression.Parameter(typeof(object), "dic");
            var keyParaExp = Expression.Parameter(typeof(object), "key");
            var valueParaExp = Expression.Parameter(typeof(object), "value");
            var dicExp = Expression.TypeAs(objExp, Type);
            var keyExp = Expression.Convert(keyParaExp, KeyType);
            var valueExp = Expression.Convert(valueParaExp, ValueType);
            //調用索引器指派
            var property = type.GetProperty("Item", new Type[] { KeyType });
            var indexExp = Expression.MakeIndex(dicExp, property, new Expression[] { keyExp });
            var body = Expression.Assign(indexExp, valueExp);
            var expression = Expression.Lambda<Action<object, object, object>>(body, objExp, keyParaExp, valueParaExp);
            return expression.Compile();
        }      

這個無傳回值的委托有3個object類型的入參,都通過Expression.Parameter定義,再分别轉換成各自真實的資料類型,然後反射找到索引器對應的PropertyInfo:type.GetProperty("Item", new Type[] { KeyType })(索引器預設屬性名為Item),得到索引器Expression.MakeIndex(dicExp, property, new Expression[] { keyExp }),這句話相當于讀key的值,對索引器指派的話還需要用 Expression.Assign(indexExp, valueExp)來完成,這樣通過索引器指派的委托就搞定了。字典根據key擷取value值的委托:Func<object, object, object>邏輯與指派操作基本相同,隻需要将索引器拿到的結果傳回就完事,代碼就不貼了。

4.4 可疊代類型(實作IEnumerable接口的類型)解析器EnumerableConverter

   實作了IEnumerable接口的類型與JsonArray之間的互轉主要用到了2個功能的委托:Func<object, IEnumerator> GetEnumerator和Action<object, object> AddItem,分别相當于讀和寫,讀是拿到IEnumerable的疊代器GetEnumerator(),然後周遊疊代器;寫是對集合添加元素,最終是集合調用自己的”Add“方法,由于不是所有集合添加資料的方法名字都叫Add,是以EnumerableConverter是一個抽象類,隻實作了公共邏輯部分,具體實作由具體實作類來完成(比如:ListConverter,ArrayConverter...)。貼上擷取疊代器委托的生成代碼與集合添加資料委托的生成代碼:

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1         protected virtual Func<object, IEnumerator> BuildGetEnumberatorMethod(Type type)
 2         {
 3             var paramExp = Expression.Parameter(typeof(object), "list");
 4             var listExp = Expression.TypeAs(paramExp, type);
 5             var method = type.GetMethod(nameof(IEnumerable.GetEnumerator));//實作了IEnumerable的類一定有GetEnumerator方法
 6             var callExp = Expression.Call(listExp, method); //調用GetEnumerator()方法
 7             var body = Expression.TypeAs(callExp, typeof(IEnumerator)); //結果轉換為IEnumerator類型
 8             var expression = Expression.Lambda<Func<object, IEnumerator>>(body, paramExp);  
 9             return expression.Compile();
10         }      

BuildGetEnumberatorMethod

使用ExpressionTree實作JSON解析器
使用ExpressionTree實作JSON解析器
1         protected virtual Action<object, object> BuildAddItemMethod(Type type)
 2         {
 3             var listExp = Expression.Parameter(typeof(object), "list");
 4             var itemExp = Expression.Parameter(typeof(object), "item");
 5             var instanceExp = Expression.Convert(listExp, type);
 6             var argumentExp = Expression.Convert(itemExp, ItemType);
 7             var addMethod = type.GetMethod(AddMethodName);//添加資料方法AddMethodName有實作的子類去指定,預設為Add
 8             var callExp = Expression.Call(instanceExp, addMethod, argumentExp); //調用添加資料方法
 9             Expression<Action<object, object>> addItemExp = Expression.Lambda<Action<object, object>>(callExp, listExp, itemExp);
10             return addItemExp.Compile();
11         }      

BuildAddItemMethod

   使用EnumerableConverter序列化對象時隻需要調用GetEnumerator委托,拿到疊代器IEnumerator,周遊疊代器将每個item輸出到json就可以了。反序列化對象時執行AddItem委托就等于集合調用自己添加資料的方法,進而完成對集合資料的填充。但是數組是不可變的,沒有添加元素的方法如何處理呢?這裡的處理方法是數組的構造先由List來完成,添加資料就可以用List.Add方法了,到最後統一調用List的ToArray()方法轉換成目标數組。是以ArrayConverter是繼承自ListConverter的,重寫一下父類ListConverter的反序列化方法,在父類處理完後調用list的ToArray方法就完成了。

  還有一大堆具體的實作這裡也不去介紹了,主要是把表達式樹實作這塊的東西寫出來當作學習筆記,順便分享一下。

  寫這個項目主要是為了學習表達式樹的運用與json的解析,其中一部分設計思路參考了Newtonsoft.Json源碼,受限于本人的水準,加上項目也沒有全面的測試,裡面一定有不少問題,歡迎大佬們提出指正,希望能與大家共同學習進步。最後希望疫情早日結束,能早點回去搬磚。

  貼上源碼位址:https://github.com/zhangmingjian/RapidityJson