今年的春節與往年不同,對每個人來說都是刻骨銘心的。突入其來的新型冠狀病毒使大家過上了“夢想”中的生活,如今這樣的生活令人一點都不踏實,隻有不停的學習才能讓人安心。于是我把年前弄了一點的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 }
從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 }
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