用ExpressionTree實現JSON解析器

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

先展示一下使用方法:

 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枚舉定義:

 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() :

 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         }
View Code

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

 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     
View Code

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

  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

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

  

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

1 public interface IConverterCreator
2     {
3         bool CanConvert(Type type);
4 
5         ITypeConverter Create(Type type);
6     }
View Code

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

 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     }
View Code

爲了能夠擴展使用自定義實現的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的對象是調用,委託的是使用表達式樹編譯生成:

 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         }
View Code

這個方法首先判斷該類型是否有無參的構造函數,如果有就直接通過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委託生成的代碼:

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         }
View Code

首先定義好方法的參數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委託的生成邏輯:

 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         }
View Code

賦值操作不需要有返回值,第一個參數是實例對象,第二個參數是成員對象,都通過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; 來看一下表達式生成這個委託的代碼:

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();
        }
View Code

這個無返回值的委託有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...)。貼上獲取迭代器委託的生成代碼與集合添加數據委託的生成代碼:

 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
 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章