今年的春節與往年不同,對每個人來說都是刻骨銘心的。突入其來的新型冠狀病毒使大家過上了“夢想”中的生活:吃了睡,睡了吃,還不用去公司上班,如今這樣的生活就在我們面前,可一點都不踏實,只有不停的學習才能讓人安心。於是我把年前弄了一點的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 }
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 }
字符串掃描方法 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 }
從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
....等等其他方法的跳轉和格式的校驗都是採用類似方法處理的。
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 }
使用這個工廠創建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 }
爲了能夠擴展使用自定義實現的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 }
這個方法首先判斷該類型是否有無參的構造函數,如果有就直接通過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 }
首先定義好方法的參數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 }
賦值操作不需要有返回值,第一個參數是實例對象,第二個參數是成員對象,都通過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(); }
這個無返回值的委託有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 }
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 }
使用EnumerableConverter序列化對象時只需要調用GetEnumerator委託,拿到迭代器IEnumerator,遍歷迭代器將每個item輸出到json就可以了。反序列化對象時執行AddItem委託就等於集合調用自己添加數據的方法,從而完成對集合數據的填充。但是數組是不可變的,沒有添加元素的方法如何處理呢?這裏的處理方法是數組的構造先由List來完成,添加數據就可以用List.Add方法了,到最後統一調用List的ToArray()方法轉換成目標數組。所以ArrayConverter是繼承自ListConverter的,重寫一下父類ListConverter的反序列化方法,在父類處理完後調用list的ToArray方法就完成了。
還有一大堆具體的實現這裏也不去介紹了,主要是把表達式樹實現這塊的東西寫出來當作學習筆記,順便分享一下。
寫這個項目主要是爲了學習表達式樹的運用與json的解析,其中一部分設計思路參考了Newtonsoft.Json源碼,受限於本人的水平,加上項目也沒有全面的測試,裏面一定有不少問題,歡迎大佬們提出指正,希望能與大家共同學習進步。最後希望疫情早日結束,能早點回去搬磚。