用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 }
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 to 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 } 15View 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/Rapidity