動態生成類在對於O/R Mapping來說有很大的作用,在實際應用中用到的一些場景,比如支持用戶自定義字段,自定義公式的解析、動態插件等等。
下面就詳細介紹一下Emit的實際應用:
Emit動態創建類型
目標:創建類型Employee,兩個簡單屬性:Name(姓名),BirthDate(生日) ,一個方法void PropertyChanged(string propertyName)
其中Name必填,預設值“新同事”,String類型;BirthDate是日期類型
准備工作:定義類型EntityDictionary和DynamicField,用於動態產生實體類型。
public class EntityDictionary { private string _name; /// <summary> /// 實體名稱 /// </summary> public string Name { get { return _name; } set { _name = value; } } private string _caption; /// <summary> /// 顯示值 /// </summary> public string Caption { get { return _caption; } set { _caption = value; } } private List<string> _interfaceList; /// <summary> /// 接口列表 /// </summary> public List<string> InterfaceList { get { return _interfaceList; } set { _interfaceList = value; } } private List<DynamicField> _dynamicFieldList; /// <summary> /// 字段列表 /// </summary> public List<DynamicField> DynamicFieldList { get { return _dynamicFieldList; } set { _dynamicFieldList = value; } } }
public class DynamicField { private string _name; /// <summary> /// 字段名稱 /// </summary> public string Name { get { return _name; } set { _name = value; } } private string _entityName; /// <summary> /// 實體名稱 /// </summary> public string EntityName { get { return _entityName; } set { _entityName = value; } } private string _type; /// <summary> /// 類型 /// </summary> public string Type { get { return _type; } set { _type = value; } } private int _size; /// <summary> /// 長度 /// </summary> public int Size { get { return _size; } set { _size = value; } } private byte _scale; /// <summary> /// 小數位數 /// </summary> public byte Scale { get { return _scale; } set { _scale = value; } } private string _refType; /// <summary> /// 引用類型 /// </summary> public string RefType { get { return _refType; } set { _refType = value; } } private string _control; /// <summary> /// 顯示控件類型 /// </summary> public string Control { get { return _control; } set { _control = value; } } private string _caption; /// <summary> /// 顯示值 /// </summary> public string Caption { get { return _caption; } set { _caption = value; } } private bool _isRequired; /// <summary> /// 是否必填 /// </summary> public bool IsRequired { get { return _isRequired; } set { _isRequired = value; } } private string _defaultValue; /// <summary> /// 默認值 /// </summary> public string DefaultValue { get { return _defaultValue; } set { _defaultValue = value; } } private string _externInfo; /// <summary> /// 擴展信息 /// </summary> public string ExternInfo { get { return _externInfo; } set { _externInfo = value; } } private object _tempProperty; /// <summary> /// 保留欄位 /// </summary> public object TempProperty { get { return _tempProperty; } set { _tempProperty = value; } } }
1.創建類
首先我們看下產生dll的代碼:
public void Create() { AppDomain myDomain = Thread.GetDomain(); AssemblyName myAsmName = new AssemblyName(); myAsmName.Name = "TestDynamicAssembly"; myAsmName.Version = new Version("1.0.0.0");
//創建一個永久程序集,設置為AssemblyBuilderAccess.RunAndSave。 AssemblyBuilder myAsmBuilder = myDomain.DefineDynamicAssembly(myAsmName, AssemblyBuilderAccess.RunAndSave); //設置版本號信息 myAsmBuilder.DefineVersionInfoResource("Test", myAsmName.Version.ToString(), "TestCorp", "Copyright © TestCorp Limited 2014", ""); //創建一個永久單模程序塊。 ModuleBuilder myModBuilder = myAsmBuilder.DefineDynamicModule(myAsmName.Name, myAsmName.Name + ".dll"); //通常采用Xml讀取方式給DynamicField的屬性賦值,這裡Demo采用簡單的直接賦值方式 List<DynamicField> listDynamicField = new List<DynamicField>(); DynamicField df = new DynamicField(); df = new DynamicField(); df.Name = "Name"; df.Caption = "姓名"; df.Type = "System.String"; df.IsRequired = true;//是否必填 df.DefaultValue = "新同事";//預設值 df.Size = 200;//字段長度 listDynamicField.Add(df); df.Name = "BirthDate"; df.Caption = "生日"; df.Type = "System.DateTime"; listDynamicField.Add(df); //通常采用Xml讀取方式給EntityDictionary的屬性賦值,這裡Demo采用簡單的直接賦值方式 EntityDictionary ed = new EntityDictionary(); ed.Name = "Employee"; ed.Caption = "雇員信息"; ed.DynamicFieldList = listDynamicField;
//創建類型 CreateEntity(myModBuilder, ed);
//創建方法 AddMethordToTypeBuilder(myModBuilder); //保存程序集。 myAsmBuilder.Save(myAsmName.Name + ".dll"); }
主要代碼CreateEntity(創建類型)如下:
private void CreateEntity(ModuleBuilder myModBuilder, EntityDictionary ed) { //重寫命名空間 string typeName = string.Format("{0}.{1}", "TestDemo.DataEntity", ed.Name); //創建TypeBuilder。 TypeBuilder myTypeBuilder = myModBuilder.DefineType(typeName, TypeAttributes.Public); #region 類標記 //序列化 CustomAttributeBuilder customAttributeBuilder = new CustomAttributeBuilder(typeof(SerializableAttribute).GetConstructor(Type.EmptyTypes), new Type[] { }); myTypeBuilder.SetCustomAttribute(customAttributeBuilder); //這是個相對復雜的類型標記 定義了類型的主鍵和別名,有興趣可以研究一下 //if (!string.IsNullOrEmpty(df.Alias)) { // customAttributeBuilder = new CustomAttributeBuilder(typeof(DataEntityAttribute).GetConstructor(Type.EmptyTypes), new Type[] { }, new PropertyInfo[] { typeof(DataEntityAttribute).GetProperty("PrimaryKey"), typeof(DataEntityAttribute).GetProperty("Alias") }, new object[] { df.PrimaryKey, df.Alias }); //} else { // customAttributeBuilder = new CustomAttributeBuilder(typeof(DataEntityAttribute).GetConstructor(Type.EmptyTypes), new Type[] { }, new PropertyInfo[] { typeof(DataEntityAttribute).GetProperty("PrimaryKey") }, new object[] { df.PrimaryKey }); //} //myTypeBuilder.SetCustomAttribute(customAttributeBuilder); #endregion #region 接口 //對類型添加接口 //if (df.InterfaceList != null && df.InterfaceList.Count > 0) { // AddInterface(myTypeBuilder, df.InterfaceList); //} #endregion #region 常量和變量 FieldBuilder fb = myTypeBuilder.DefineField("TYPEKEY", typeof(string), FieldAttributes.Public | FieldAttributes.FamANDAssem | FieldAttributes.Family | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault); fb.SetConstant(ed.Name); #endregion //添加屬性到TypeBuilder。 AddPropertyToTypeBuilder(myTypeBuilder, ed.DynamicFieldList); myTypeBuilder.CreateType(); }
到這裡,我們已經創建了dll的版本信息,類型的命名空間、類標記、常量等信息,此時如果排除添加屬性的方法,已經可以編譯成dll了,反編譯的結果如下:
private void AddPropertyToTypeBuilder(TypeBuilder myTypeBuilder, List<DynamicField> dfList) { PropertyBuilder custNamePropBldr; MethodBuilder custNameGetPropMthdBldr; MethodBuilder custNameSetPropMthdBldr; MethodAttributes getSetAttr; ILGenerator custNameGetIL; ILGenerator custNameSetIL; // 屬性Set和Get方法要一個專門的屬性。這裡設置為Public。 getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; foreach (DynamicField df in dfList) { try { string fieldPrivateName = "_" + df.Name.Substring(0, 1).ToLower() + df.Name.Substring(1); //定義字段。 FieldBuilder customerNameBldr = myTypeBuilder.DefineField(fieldPrivateName,Type.GetType(df.Type), FieldAttributes.Private); //定義屬性。 custNamePropBldr = myTypeBuilder.DefineProperty(df.Name, System.Reflection.PropertyAttributes.RTSpecialName, Type.GetType(df.Type), null); CustomAttributeBuilder customAttributeBuilder = new CustomAttributeBuilder(typeof(DescriptionAttribute).GetConstructor(new Type[] { typeof(string) }), new object[] { df.Caption }); custNamePropBldr.SetCustomAttribute(customAttributeBuilder);//字段描述 #region 增加必填字段屬性和特殊默認值屬性 if (df.IsRequired) { customAttributeBuilder = new CustomAttributeBuilder(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute).GetConstructor(Type.EmptyTypes), new object[] { }); custNamePropBldr.SetCustomAttribute(customAttributeBuilder);//是否必填 } if (!string.IsNullOrEmpty(df.DefaultValue)) { customAttributeBuilder = new CustomAttributeBuilder(typeof(DefaultValueAttribute).GetConstructor(new Type[] { typeof(string) }), new object[] { df.DefaultValue }); custNamePropBldr.SetCustomAttribute(customAttributeBuilder);//特殊默認值 } #endregion //定義Get方法。 custNameGetPropMthdBldr = myTypeBuilder.DefineMethod("get_" + df.Name, getSetAttr, Type.GetType(df.Type), Type.EmptyTypes); custNameGetIL = custNameGetPropMthdBldr.GetILGenerator(); custNameGetIL.Emit(OpCodes.Ldarg_0); custNameGetIL.Emit(OpCodes.Ldfld, customerNameBldr); custNameGetIL.Emit(OpCodes.Ret); //把創建的兩個方法(Get,Set)加入到PropertyBuilder中。 custNamePropBldr.SetGetMethod(custNameGetPropMthdBldr); //定義Set方法。 custNameSetPropMthdBldr = myTypeBuilder.DefineMethod("set_" + df.Name, getSetAttr, null, new Type[] { Type.GetType(df.Type) }); custNameSetIL = custNameSetPropMthdBldr.GetILGenerator(); custNameSetIL.Emit(OpCodes.Ldarg_0); custNameSetIL.Emit(OpCodes.Ldarg_1); custNameSetIL.Emit(OpCodes.Stfld, customerNameBldr); custNameSetIL.Emit(OpCodes.Ret); //把創建的兩個方法(Get,Set)加入到PropertyBuilder中。 custNamePropBldr.SetSetMethod(custNameSetPropMthdBldr); } catch (Exception ex) { throw new Exception("AddPropertyToTypeBuilder Error:" + ex.Message); } } }
再生成dll反編譯,結果如下:
public void PropertyChanged(string propertyName) { if (propertyName != null) {//判斷語句 Console.WriteLine(propertyName + " Changed!"); } }
然後生成dll,利用反編譯工具得到這段代碼的IL語言
private void AddMethordToTypeBuilder(TypeBuilder myTypeBuilder) { MethodBuilder methodBuilder = myTypeBuilder.DefineMethod("PropertyChanged", MethodAttributes.Public, null, new Type[] { typeof(string) }); ILGenerator il = methodBuilder.GetILGenerator(); Label lb_001 = il.DefineLabel();//lable il.Emit(OpCodes.Nop); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Ceq); il.Emit(OpCodes.Stloc_0); il.Emit(OpCodes.Ldloc_0); il.Emit(OpCodes.Brtrue_S, lb_001); il.Emit(OpCodes.Nop); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldstr, " Changed!"); il.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) })); il.Emit(OpCodes.Call, typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(string)})); il.Emit(OpCodes.Nop); il.MarkLabel(lb_001); il.Emit(OpCodes.Nop); il.Emit(OpCodes.Ret); }
再生成dll,反編譯看看結果:
//創建TypeBuilder。 TypeBuilder myTypeBuilder = myModBuilder.DefineType(typeName, TypeAttributes.Public); myTypeBuilder.SetParent(type);//繼承
myTypeBuilder.AddInterfaceImplementation(typeInterface);//接口
需要注意的是,添加接口必須要添加接口實現的方法或者屬性,否則編譯會失敗。
Emit應用場景的開發思路
一開始提到的Emit應用場景,實現用戶級自定義字段。
步驟1.新增用戶自定義字段的工具,其實就是維護一份或多份Xml,記錄基類實體的Key和新增的字段等信息。並提供按鈕生成dll。
上述Demo中的字段都是可以通過Xml來讀取相關信息,包括類標記、屬性、屬性標記、基類、接口等,還可以在工具上新增字段對應的控件布局的編輯器,用於保存控件布局。
步驟2.在程序中通過反射引用生成的dll,並在注冊實體和類型的關系字典時增加特殊判斷。
拿上面的Employee舉例,首先我們的程序中需要繼承EmployeeEx:Employee,在O\R Mapping中,
實體的增刪改查通常是封裝好的方法,例如Create("Employee") 後台會通過"Employee"找到類型Employee做實例化,
這就需要一開始對實體和實體的Key放到一個字典中,如"Employee"對應Employee,"Job"對應Job。如果我們在建立對應關系的字典時發現有Emit動態產生的dll,將相應的對應關系做修改,
如"Employee"對應EmployeeEx,那個在Create("Employee")的時候,後台會通過"Employee"找到類型EmployeeEx做實例化。這樣的話不管有沒有用戶自定義列,代碼都不用修改。
步驟3.在客戶端界面自動生成維護控件並綁定,以便用戶維護。
客戶端可以新增一個面板控件,用於動態加入控件和綁定字段,如果在步驟1中添加了自定義布局,還可以導入控件布局,再進行綁定。
這樣,用戶就可以不用改到代碼就新增自己需要的字段。這對於一個標准產品來說,解決了不同客戶的一些簡單並且常用的客制需求。
一開始提到的Emit應用場景,實現自定義公式解析。
舉例:用戶可以自定義公式,改公式應用於不同員工會得到不同的值。公式內容如下:
公式名稱:請假扣款 返回類型:數字型 公式內容: if 請假次數==0 return 0; else if 請假次數<10 return 請假扣款系數*請假次數;
else
return 函數A(請假次數);
步驟1.定義系統級參數if,else,return等,
步驟2.定義自定義函數,並提供固定解析方法。(如 decimal 函數A(int)、日期函數等)
步驟3.提供一系列參數,可動態產生。(如 請假次數) 動態編譯類型類型請假次數,繼承int
步驟4.(最難的一步)將公式解析成動態方法,裡面的參數作為方法的變量,遇到函數則使用Emit中的Call。
步驟5.解析公式最後的結果,如員工A,程序運行到公式時,首先通過員工A得到參數的值,即請假扣款系數和請假次數,再把值傳入動態方法,最終得到公式的值。
當然,具體實做會遇到很多細節上的困難,這裡的步驟只是簡單的介紹。
總結
面對形形色色的用戶需求,我們需要做出一些靈活多變的功能,Emit技術為我們解決問題提供了思路。
BCB中利用__emit__函數可以直接將二進制程序代碼嵌入程序中,這樣就可以實現一些底層的操作。由於直接操作系統底層,這種方法可能會導致系統的不穩定。
下面是利用__emit__函數讀寫硬件端口的方法。
//讀端口
//port參數為輸入端口地址,value為返回值.
unsigned char __fastcall inportb(unsigned short int port)
{
unsigned char value;
__emit__(0x8b,0x95,&port); //把端口地址送到EDX寄存器中
__emit__(0x66,0xec); //從端口中讀入數據到AL寄存器中
__emit__(0x88,0x85,&value); //把AL寄存器中的值輔給value
return value;
}
//---------------------------------------------------------------------------
//寫端口
//port參數為輸出端口地址,value參數為輸出值
void __fastcall outportb(unsigned short int port,unsigned char value)
{
__emit__(0x8b,0x95,&port); //把端口地址送到EDX寄存器中
__emit__(0x8a,0x85,&value); //把value送到AL寄存器中
__emit__(0x66,0xee); //把AL寄存器中的值寫入端口
基本MapReduce模式
計數與求和
問題陳述:
有許多文檔,每個文檔都有一些字段組成。需要計算出每個字段在所有文檔中的出現次數或者這些字段的其他什麼統計值。例如,給定一個log文件,其中的每條記錄都包含一個響應時間,需要計算出平均響應時間。
解決方案:
讓我們先從簡單的例子入手。在下面的代碼片段裡,Mapper每遇到指定詞就把頻次記1,Reducer一個個遍歷這些詞的集合然後把他們的頻次加和。
1 class Mapper
2 method Map(docid id, doc d)
3 for all term t in doc d do
4 Emit(term t, count 1)
5
6 class Reducer
7 method Reduce(term t, counts [c1, c2,...])
8 sum = 0
9 for all count c in [c1, c2,...] do
10 sum = sum + c
11 Emit(term t, count sum)
這種方法的缺點顯而易見,Mapper提交了太多無意義的計數。它完全可以通過先對每個文檔中的詞進行計數從而減少傳遞給Reducer的數據量:
1 class Mapper
2 method Map(docid id, doc d)
3 H = new AssociativeArray
4 for all term t in doc d do
5 H{t} = H{t} + 1
6 for all term t in H do
7 Emit(term t, count H{t})
如果要累計計數的的不只是單個文檔中的內容,還包括了一個Mapper節點處理的所有文檔,那就要用到Combiner了:
1 class Mapper
2 method Map(docid id, doc d)
3 for all term t in doc d do
4 Emit(term t, count 1)
5
6 class Combiner
7 method Combine(term t, [c1, c2,...])
8 sum = 0
9 for all count c in [c1, c2,...] do
10 sum = sum + c
11 Emit(term t, count sum)
12
13 class Reducer
14 method Reduce(term t, counts [c1, c2,...])
15 sum = 0
16 for all count c in [c1, c2,...] do
17 sum = sum + c
18 Emit(term t, count sum)
應用:Log 分析, 數據查詢
整理歸類
問題陳述:
有一系列條目,每個條目都有幾個屬性,要把具有同一屬性值的條目都保存在一個文件裡,或者把條目按照屬性值分組。 最典型的應用是倒排索引。
解決方案:
解決方案很簡單。 在 Mapper 中以每個條......余下全文>>