(轉)Attribute在.net編程中的應用
Attribute在.net編程中的應用(一)
Attribute的基本概念
經常有朋友問,Attribute是什麽?它有什麽用?好像沒有這個東東程序也能運行。實際上在.Net中,Attribute是一個非常重要的組成部分,為了幫助大家理解和掌握Attribute,以及它的使用方法,特地收集了幾個Attribute使用的例子,提供給大家參考。
在具體的演示之前,我想先大致介紹一下Attribute。我們知道在類的成員中有property成員,二者在中文中都做屬性解釋,那麽它們到底是不是同一個東西呢?從代碼上看,明顯不同,首先就是它們的在代碼中的位置不同,其次就是寫法不同(Attribute必須寫在一對方括符中)。
什麽是Atrribute
首先,我們肯定Attribute是一個類,下面是msdn文檔對它的描述:
公共語言運行時允許你添加類似關鍵字的描述聲明,叫做attributes, 它對程序中的元素進行標註,如類型、字段、方法和屬性等。Attributes和Microsoft .NET Framework文件的元數據保存在一起,可以用來向運行時描述你的代碼,或者在程序運行的時候影響應用程序的行為。
在.NET中,Attribute被用來處理多種問題,比如序列化、程序的安全特征、防止即時編譯器對程序代碼進行優化從而代碼容易調試等等。下面,我們先來看幾個在.NET中標準的屬性的使用,稍後我們再回過頭來討論Attribute這個類本身。(文中的代碼使用C#編寫,但同樣適用所有基於.NET的所有語言)
Attribute作為編譯器的指令
在C#中存在著一定數量的編譯器指令,如:#define DEBUG, #undefine DEBUG, #if等。這些指令專屬於C#,而且在數量上是固定的。而Attribute用作編譯器指令則不受數量限制。比如下面的三個Attribute:
- Conditional:起條件編譯的作用,只有滿足條件,才允許編譯器對它的代碼進行編譯。一般在程序調試的時候使用。
- DllImport:用來標記非.NET的函數,表明該方法在一個外部的DLL中定義。
- Obsolete:這個屬性用來標記當前的方法已經被廢棄,不再使用了。
下面的代碼演示了上述三個屬性的使用:
#define DEBUG //這裏定義條件 using System; using System.Runtime.InteropServices; using System.Diagnostics; namespace AttributeDemo { class MainProgramClass { [DllImport("User32.dll")] public static extern int MessageBox(int hParent, string Message, string Caption, int Type); static void Main(string[] args) { DisplayRunningMessage(); DisplayDebugMessage(); MessageBox(0,"Hello","Message",0); Console.ReadLine(); } [Conditional("DEBUG")] private static void DisplayRunningMessage() { Console.WriteLine("開始運行Main子程序。當前時間是"+DateTime.Now); } [Conditional("DEBUG")] [Obsolete] private static void DisplayDebugMessage() { Console.WriteLine("開始Main子程序"); } } }
如果在一個程序元素前面聲明一個Attribute,那麽就表示這個Attribute被施加到該元素上,前面的代碼,[DllImport]施加到MessageBox函數上, [Conditional]施加到DisplayRuntimeMessage方法和DisplayDebugMessage方法,[Obsolete]施加到DisplayDebugMessage方法上。
根據上面涉及到的三個Attribute的說明,我們可以猜到程序運行的時候產生的輸出:DllImport Attribute表明了MessageBox是User32.DLL中的函數,這樣我們就可以像內部方法一樣調用這個函數。
重要的一點就是Attribute就是一個類,所以DllImport也是一個類,Attribute類是在編譯的時候被實例化的,而不是像通常的類那樣在運行時候才實例化。Attribute實例化的時候根據該Attribute類的設計可以帶參數,也可以不帶參數,比如DllImport就帶有"User32.dll"的參數。Conditional對滿足參數的定義條件的代碼進行編譯,如果沒有定義DEBUG,那麽該方法將不被編譯,讀者可以把#define DEBUG一行註釋掉看看輸出的結果(release版本,在Debug版本中Conditional的debug總是成立的)。Obsolete表明了DispalyDebugMessage方法已經過時了,它有一個更好的方法來代替它,當我們的程序調用一個聲明了Obsolete的方法時,那麽編譯器會給出信息,Obsolete還有其他兩個重載的版本。大家可以參考msdn中關於的ObsoleteAttribute 類的描述。
Attribute類
除了.NET提供的那些Attribute派生類之外,我們可以自定義我們自己的Attribute,所有自定義的Attribute必須從Attribute類派生。現在我們來看一下Attribute 類的細節:
protected Attribute(): 保護的構造器,只能被Attribute的派生類調用。
三個靜態方法:
static Attribute GetCustomAttribute():這個方法有8種重載的版本,它被用來取出施加在類成員上指定類型的Attribute。
static Attribute[] GetCustomAttributes(): 這個方法有16種重載版本,用來取出施加在類成員上指定類型的Attribute數組。
static bool IsDefined():由八種重載版本,看是否指定類型的定制attribute被施加到類的成員上面。
實例方法:
bool IsDefaultAttribute(): 如果Attribute的值是默認的值,那麽返回true。
bool Match():表明這個Attribute實例是否等於一個指定的對象。
公共屬性: TypeId: 得到一個唯一的標識,這個標識被用來區分同一個Attribute的不同實例。
我們簡單地介紹了Attribute類的方法和屬性,還有一些是從object繼承來的。這裏就不列出來了。
下面介紹如何自定義一個Attribute: 自定義一個Attribute並不需要特別的知識,其實就和編寫一個類差不多。自定義的Attribute必須直接或者間接地從Attribute這個類派生,如:
public MyCustomAttribute : Attribute { ... }
這裏需要指出的是Attribute的命名規範,也就是你的Attribute的類名+"Attribute",當你的Attribute施加到一個程序的元素上的時候,編譯器先查找你的Attribute的定義,如果沒有找到,那麽它就會查找“Attribute名稱"+Attribute的定義。如果都沒有找到,那麽編譯器就報錯。
對於一個自定義的Attribute,你可以通過AttributeUsage的Attribute來限定你的Attribute 所施加的元素的類型。代碼形式如下: [AttriubteUsage(參數設置)] public 自定義Attribute : Attribute { ... }
非常有意思的是,AttributeUsage本身也是一個Attribute,這是專門施加在Attribute類的Attribute. AttributeUsage自然也是從Attribute派生,它有一個帶參數的構造器,這個參數是AttributeTargets的枚舉類型。下面是AttributeTargets 的定義:
public enum AttributeTargets { All=16383, Assembly=1, Module=2, class="4", Struct=8, Enum=16, Constructor=32, Method=64, Property=128, Field=256, Event=512, Interface=1024, Parameter=2048, Delegate=4096, ReturnValue=8192 }
作為參數的AttributeTarges的值允許通過“或”操作來進行多個值得組合,如果你沒有指定參數,那麽默認參數就是All 。 AttributeUsage除了繼承Attribute 的方法和屬性之外,還定義了以下三個屬性:
AllowMultiple: 讀取或者設置這個屬性,表示是否可以對一個程序元素施加多個Attribute 。
Inherited:讀取或者設置這個屬性,表示是否施加的Attribute 可以被派生類繼承或者重載。
ValidOn: 讀取或者設置這個屬性,指明Attribute 可以被施加的元素的類型。
AttributeUsage 的使用例子:
using System; namespace AttTargsCS { // 該Attribute只對類有效. [AttributeUsage(AttributeTargets.Class)] public class ClassTargetAttribute : Attribute { } // 該Attribute只對方法有效. [AttributeUsage(AttributeTargets.Method)] public class MethodTargetAttribute : Attribute { } // 該Attribute只對構造器有效。 [AttributeUsage(AttributeTargets.Constructor)] public class ConstructorTargetAttribute : Attribute { } // 該Attribute只對字段有效. [AttributeUsage(AttributeTargets.Field)] public class FieldTargetAttribute : Attribute { } // 該Attribute對類或者方法有效(組合). [AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)] public class ClassMethodTargetAttribute : Attribute { } // 該Attribute對所有的元素有效. [AttributeUsage(AttributeTargets.All)] public class AllTargetsAttribute : Attribute { } //上面定義的Attribute施加到程序元素上的用法 [ClassTarget] //施加到類 [ClassMethodTarget]//施加到類 [AllTargets] //施加到類 public class TestClassAttribute { [ConstructorTarget] //施加到構造器 [AllTargets] //施加到構造器 TestClassAttribute() { } [MethodTarget] //施加到方法 [ClassMethodTarget] //施加到方法 [AllTargets] //施加到方法 public void Method1() { } [FieldTarget] //施加到字段 [AllTargets] //施加到字段 public int myInt; static void Main(string[] args) { } } }
至此,我們介紹了有關Attribute類和它們的代碼格式。你一定想知道到底如何在你的應用程序中使用Attribute,如果僅僅是前面介紹的內容,還是不足以說明Attribute有什麽實用價值的話,那麽從後面的章節開始我們將介紹幾個Attribute的不同用法,相信你一定會對Attribute有一個新的了解。
Attribute在.net編程中的應用(二)
.NET Framework中對Attribute的支持是一個全新的功能,這種支持來自它的Attribute類。在你的程序中適當地使用這個類,或者是靈活巧妙地利用這個類,將使你的程序獲得某種在以往編程中很難做到的能力。我們來看一個例子:
假如你是一個項目開發小組中的成員,你想要跟蹤項目代碼檢查的信息,通常你可以把代碼的檢查信息保存在數據庫中以便查詢;或者把信息寫到代碼的註釋裏面,這樣可以閱讀代碼的同時看到代碼被檢查的信息。我們知道.NET的組件是自描述的,那麽是否可以讓代碼自己來描述它被檢查的信息呢?這樣我們既可以將信息和代碼保存在一起,又可以通過代碼的自我描述得到信息。答案就是使用Attribute.
下面的步驟和代碼告訴你怎麽做:
首先,我們創建一個自定義的Attribute,並且事先設定我們的Attribute將施加在class的元素上面以獲取一個類代碼的檢查信息。
using System; using System.Reflection; [AttributeUsage(AttributeTargets.Class)] //還記得上一節的內容嗎? public class CodeReviewAttribute : System.Attribute //定義一個CodeReview的Attribute { private string reviewer; //代碼檢查人 private string date; //檢查日期 private string comment; //檢查結果信息 //參數構造器 public CodeReviewAttribute(string reviewer, string date) { this.reviewer=reviewer; this.date=date; } public string Reviewer { get { return reviewer; } } public string Date { get { return date; } } public string Comment { get { return comment; } set { comment=value; } } }
我們的自定義CodeReviewAttribute同普通的類沒有區別,它從Attribute派生,同時通過AttributeUsage表示我們的Attribute僅可以施加到類元素上。
第二步就是使用我們的CodeReviewAttribute, 假如我們有一個Jack寫的類MyClass,檢查人Niwalker,檢查日期2003年7月9日,於是我們施加Attribute如下:
[CodeReview("Niwalker","2003-7-9",Comment="Jack的代碼")] public class MyClass { //類的成員定義 }
當這段代碼被編譯的時候,編譯器會調用CodeReviewAttribute的構造器並且把"Niwalker"和"2003-7-9"分別作為構造器的參數。註意到參數表中還有一個Comment屬性的賦值,這是Attribute特有的方式,這裏你可以設置更多的Attribute的公共屬性(如果有的話),需要指出的是.NET Framework1.0允許向private的屬性賦值,但在.NET Framework1.1已經不允許這樣做,只能向public的屬性賦值。
第三步就是取出我們需要的信息,這是通過.NET的反射來實現的,關於反射的知識,限於篇幅我不打算在這裏進行說明,也許我會在以後另外寫一篇介紹反射的文章。
class test { static void Main(string[] args) { System.Reflection.MemberInfo info=typeof(MyClass); //通過反射得到MyClass類的信息 //得到施加在MyClass類上的定制Attribute CodeReviewAttribute att= (CodeReviewAttribute)Attribute.GetCustomAttribute(info,typeof(CodeReviewAttribute)); if(att!=null) { Console.WriteLine("代碼檢查人:{0}",att.Reviewer); Console.WriteLine("檢查時間:{0}",att.Date); Console.WriteLine("註釋:{0}",att.Comment); } } }
在上面這個例子中,Attribute扮演著向一個類添加額外信息的角色,它並不影響MyClass類的行為。通過這個例子,我們大致可以知道如何寫一個自定義的Attribute,以及如何在應用程序使用它。下一節,我將介紹如何使用Attribute來自動生成ADO.NET的數據訪問類的代碼。
Attribute在.NET編程中的應用(三)
用於參數的Attribute
在編寫多層應用程序的時候,你是否為每次要寫大量類似的數據訪問代碼而感到枯燥無味?比如我們需要編寫調用存儲過程的代碼,或者編寫T_SQL代碼,這些代碼往往需要傳遞各種參數,有的參數個數比較多,一不小心還容易寫錯。有沒有一種一勞永逸的方法?當然,你可以使用MS的Data Access Application Block,也可以使用自己編寫的Block。這裏向你提供一種另類方法,那就是使用Attribute。
下面的代碼是一個調用AddCustomer存儲過程的常規方法:
public int AddCustomer(SqlConnection connection, string customerName, string country, string province, string city, string address, string telephone) { SqlCommand command=new SqlCommand("AddCustomer", connection); command.CommandType=CommandType.StoredProcedure; command.Parameters.Add("@CustomerName",SqlDbType.NVarChar,50).Value=customerName; command.Parameters.Add("@country",SqlDbType.NVarChar,20).Value=country; command.Parameters.Add("@Province",SqlDbType.NVarChar,20).Value=province; command.Parameters.Add("@City",SqlDbType.NVarChar,20).Value=city; command.Parameters.Add("@Address",SqlDbType.NVarChar,60).Value=address; command.Parameters.Add("@Telephone",SqlDbType.NvarChar,16).Value=telephone; command.Parameters.Add("@CustomerId",SqlDbType.Int,4).Direction=ParameterDirection.Output; connection.Open(); command.ExecuteNonQuery(); connection.Close(); int custId=(int)command.Parameters["@CustomerId"].Value; return custId; }
上面的代碼,創建一個Command實例,然後添加存儲過程的參數,然後調用ExecuteMonQuery方法執行數據的插入操作,最後返回CustomerId。從代碼可以看到參數的添加是一種重復單調的工作。如果一個項目有100多個甚至幾百個存儲過程,作為開發人員的你會不會要想辦法偷懶?(反正我會的:-))。
下面開始我們的代碼自動生成工程:
我們的目的是根據方法的參數以及方法的名稱,自動生成一個Command對象實例,第一步我們要做的就是創建一個SqlParameterAttribute, 代碼如下:
SqlCommandParameterAttribute.cs using System; using System.Data; using Debug=System.Diagnostics.Debug; namespace DataAccess { // SqlParemeterAttribute 施加到存儲過程參數 [ AttributeUsage(AttributeTargets.Parameter) ] public class SqlParameterAttribute : Attribute { private string name; //參數名稱 private bool paramTypeDefined; //是否參數的類型已經定義 private SqlDbType paramType; //參數類型 private int size; //參數尺寸大小 private byte precision; //參數精度 private byte scale; //參數範圍 private bool directionDefined; //是否定義了參數方向 private ParameterDirection direction; //參數方向 public SqlParameterAttribute() { } public string Name { get { return name == null ? string.Empty : name; } set { _name = value; } } public int Size { get { return size; } set { size = value; } } public byte Precision { get { return precision; } set { precision = value; } } public byte Scale { get { return scale; } set { scale = value; } } public ParameterDirection Direction { get { Debug.Assert(directionDefined); return direction; } set { direction = value; directionDefined = true; } } public SqlDbType SqlDbType { get { Debug.Assert(paramTypeDefined); return paramType; } set { paramType = value; paramTypeDefined = true; } } public bool IsNameDefined { get { return name != null && name.Length != 0; } } public bool IsSizeDefined { get { return size != 0; } } public bool IsTypeDefined { get { return paramTypeDefined; } } public bool IsDirectionDefined { get { return directionDefined; } } public bool IsScaleDefined { get { return _scale != 0; } } public bool IsPrecisionDefined { get { return _precision != 0; } } ...
以上定義了SqlParameterAttribute的字段和相應的屬性,為了方便Attribute的使用,我們重載幾個構造器,不同的重載構造器用於不用的參數:
... // 重載構造器,如果方法中對應於存儲過程參數名稱不同的話,我們用它來設置存儲過程的名稱 // 其他構造器的目的類似 public SqlParameterAttribute(string name) { Name=name; } public SqlParameterAttribute(int size) { Size=size; } public SqlParameterAttribute(SqlDbType paramType) { SqlDbType=paramType; } public SqlParameterAttribute(string name, SqlDbType paramType) { Name = name; SqlDbType = paramType; } public SqlParameterAttribute(SqlDbType paramType, int size) { SqlDbType = paramType; Size = size; } public SqlParameterAttribute(string name, int size) { Name = name; Size = size; } public SqlParameterAttribute(string name, SqlDbType paramType, int size) { Name = name; SqlDbType = paramType; Size = size; } } }
為了區分方法中不是存儲過程參數的那些參數,比如SqlConnection,我們也需要定義一個非存儲過程參數的Attribute:
//NonCommandParameterAttribute.cs using System; namespace DataAccess { [ AttributeUsage(AttributeTargets.Parameter) ] public sealed class NonCommandParameterAttribute : Attribute { } }
我們已經完成了SQL的參數Attribute的定義,在創建Command對象生成器之前,讓我們考慮這樣的一個事實,那就是如果我們數據訪問層調用的不是存儲過程,也就是說Command的CommandType不是存儲過程,而是帶有參數的SQL語句,我們想讓我們的方法一樣可以適合這種情況,同樣我們仍然可以使用Attribute,定義一個用於方法的Attribute來表明該方法中的生成的Command的CommandType是存儲過程還是SQL文本,下面是新定義的Attribute的代碼:
//SqlCommandMethodAttribute.cs using System; using System.Data; namespace Emisonline.DataAccess { [AttributeUsage(AttributeTargets.Method)] public sealed class SqlCommandMethodAttribute : Attribute { private string commandText; private CommandType commandType; public SqlCommandMethodAttribute( CommandType commandType, string commandText) { commandType=commandType; commandText=commandText; } public SqlCommandMethodAttribute(CommandType commandType) : this(commandType, null){} public string CommandText { get { return commandText==null ? string.Empty : commandText; } set { commandText=value; } } public CommandType CommandType { get { return commandType; } set { commandType=value; } } } }
我們的Attribute的定義工作已經全部完成,下一步就是要創建一個用來生成Command對象的類。
Attribute在.NET編程中的應用(四)
SqlCommandGenerator類的設計
SqlCommandGEnerator類的設計思路就是通過反射得到方法的參數,使用被SqlCommandParameterAttribute標記的參數來裝配一個Command實例。
引用的命名空間:
//SqlCommandGenerator.cs using System; using System.Reflection; using System.Data; using System.Data.SqlClient; using Debug = System.Diagnostics.Debug; using StackTrace = System.Diagnostics.StackTrace;
類代碼:
namespace DataAccess { public sealed class SqlCommandGenerator { //私有構造器,不允許使用無參數的構造器構造一個實例 private SqlCommandGenerator() { throw new NotSupportedException(); } //靜態只讀字段,定義用於返回值的參數名稱 public static readonly string ReturnValueParameterName = "RETURN_VALUE"; //靜態只讀字段,用於不帶參數的存儲過程 public static readonly object[] NoValues = new object[] {}; public static SqlCommand GenerateCommand(SqlConnection connection, MethodInfo method, object[] values) { //如果沒有指定方法名稱,從堆棧幀得到方法名稱 if (method == null) method = (MethodInfo) (new StackTrace().GetFrame(1).GetMethod()); // 獲取方法傳進來的SqlCommandMethodAttribute // 為了使用該方法來生成一個Command對象,要求有這個Attribute。 SqlCommandMethodAttribute commandAttribute = (SqlCommandMethodAttribute) Attribute.GetCustomAttribute(method, typeof(SqlCommandMethodAttribute)); Debug.Assert(commandAttribute != null); Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure || commandAttribute.CommandType == CommandType.Text); // 創建一個SqlCommand對象,同時通過指定的attribute對它進行配置。 SqlCommand command = new SqlCommand(); command.Connection = connection; command.CommandType = commandAttribute.CommandType; // 獲取command的文本,如果沒有指定,那麽使用方法的名稱作為存儲過程名稱 if (commandAttribute.CommandText.Length == 0) { Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure); command.CommandText = method.Name; } else { command.CommandText = commandAttribute.CommandText; } // 調用GeneratorCommandParameters方法,生成command參數,同時添加一個返回值參數 GenerateCommandParameters(command, method, values); command.Parameters.Add(ReturnValueParameterName, SqlDbType.Int).Direction =ParameterDirection.ReturnValue; return command; } private static void GenerateCommandParameters( SqlCommand command, MethodInfo method, object[] values) { // 得到所有的參數,通過循環一一進行處理。 ParameterInfo[] methodParameters = method.GetParameters(); int paramIndex = 0; foreach (ParameterInfo paramInfo in methodParameters) { // 忽略掉參數被標記為[NonCommandParameter ]的參數 if (Attribute.IsDefined(paramInfo, typeof(NonCommandParameterAttribute))) continue; // 獲取參數的SqlParameter attribute,如果沒有指定,那麽就創建一個並使用它的缺省設置。 SqlParameterAttribute paramAttribute = (SqlParameterAttribute) Attribute.GetCustomAttribute( paramInfo, typeof(SqlParameterAttribute)); if (paramAttribute == null) paramAttribute = new SqlParameterAttribute(); //使用attribute的設置來配置一個參數對象。使用那些已經定義的參數值。如果沒有定義,那麽就從方法 // 的參數來推斷它的參數值。 SqlParameter sqlParameter = new SqlParameter(); if (paramAttribute.IsNameDefined) sqlParameter.ParameterName = paramAttribute.Name; else sqlParameter.ParameterName = paramInfo.Name; if (!sqlParameter.ParameterName.StartsWith("@")) sqlParameter.ParameterName = "@" + sqlParameter.ParameterName; if (paramAttribute.IsTypeDefined) sqlParameter.SqlDbType = paramAttribute.SqlDbType; if (paramAttribute.IsSizeDefined) sqlParameter.Size = paramAttribute.Size; if (paramAttribute.IsScaleDefined) sqlParameter.Scale = paramAttribute.Scale; if (paramAttribute.IsPrecisionDefined) sqlParameter.Precision = paramAttribute.Precision; if (paramAttribute.IsDirectionDefined) { sqlParameter.Direction = paramAttribute.Direction; } else { if (paramInfo.ParameterType.IsByRef) { sqlParameter.Direction = paramInfo.IsOut ? ParameterDirection.Output : ParameterDirection.InputOutput; } else { sqlParameter.Direction = ParameterDirection.Input; } } // 檢測是否提供的足夠的參數對象值 Debug.Assert(paramIndex < values.Length); //把相應的對象值賦於參數。 sqlParameter.Value = values[paramIndex]; command.Parameters.Add(sqlParameter); paramIndex++; } //檢測是否有多余的參數對象值 Debug.Assert(paramIndex == values.Length); } } }
必要的工作終於完成了。SqlCommandGenerator中的代碼都加上了註釋,所以並不難讀懂。下面我們進入最後的一步,那就是使用新的方法來實現上一節我們一開始顯示個那個AddCustomer的方法。
重構新的AddCustomer代碼:
[ SqlCommandMethod(CommandType.StoredProcedure) ] public void AddCustomer( [NonCommandParameter] SqlConnection connection, [SqlParameter(50)] string customerName, [SqlParameter(20)] string country, [SqlParameter(20)] string province, [SqlParameter(20)] string city, [SqlParameter(60)] string address, [SqlParameter(16)] string telephone, out int customerId ) { customerId=0; //需要初始化輸出參數 //調用Command生成器生成SqlCommand實例 SqlCommand command = SqlCommandGenerator.GenerateCommand( connection, null, new object[] {customerName,country,province,city,address,telephone,customerId } ); connection.Open(); command.ExecuteNonQuery(); connection.Close(); //必須明確返回輸出參數的值 customerId=(int)command.Parameters["@CustomerId"].Value; }
代碼中必須註意的就是out參數,需要事先進行初始化,並在Command執行操作以後,把參數值傳回給它。受益於Attribute,使我們擺脫了那種編寫大量枯燥代碼編程生涯。 我們甚至還可以使用Sql存儲過程來編寫生成整個方法的代碼,如果那樣做的話,可就大大節省了你的時間了,上一節和這一節中所示的代碼,你可以把它們單獨編譯成一個組件,這樣就可以在你的項目中不斷的重用它們了。從下一節開始,我們將更深層次的介紹Attribute的應用,請繼續關註。
Attribute在.NET編程中的應用(五)
Attribute在攔截機制上的應用
從這一節開始我們討論Attribute的高級應用,為此我準備了一個實際的例子:我們有一個訂單處理系統,當一份訂單提交的時候,系統檢查庫存,如果庫存存量滿足訂單的數量,系統記錄訂單處理記錄,然後更新庫存,如果庫存存量低於訂單的數量,系統做相應的記錄,同時向庫存管理員發送郵件。為了方便演示,我們對例子進行了簡化:
//Inventory.cs using System; using System.Collections; namespace NiwalkerDemo { public class Inventory { private Hashtable inventory=new Hashtable(); public Inventory() { inventory["Item1"]=100; inventory["Item2"]=200; } public bool Checkout(string product, int quantity) { int qty=GetQuantity(product); return qty>=quantity; } public int GetQuantity(string product) { int qty=0; if(inventory[product]!=null) qty = (int)inventory[product]; return qty; } public void Update(string product, int quantity) { int qty=GetQuantity(product); inventory[product]=qty-quantity; } } } //Logbook.cs using System; namespace NiwalkerDemo { public class Logbook { public static void Log(string logData) { Console.WriteLine("log:{0}",logData); } } } //Order.cs using System; namespace NiwalkerDemo { public class Order { private int orderId; private string product; private int quantity; public Order(int orderId) { this.orderId=orderId; } public void Submit() { Inventory inventory=new Inventory(); //創建庫存對象 //檢查庫存 if(inventory.Checkout(product,quantity)) { Logbook.Log("Order"+orderId+" available"); inventory.Update(product,quantity); } else { Logbook.Log("Order"+orderId+" unavailable"); SendEmail(); } } public string ProductName { get{ return product; } set{ product=value; } } public int OrderId { get{ return orderId; } } public int Quantity { get{ return quantity;} set{ quantity=value; } } public void SendEmail() { Console.WriteLine("Send email to manager"); } } }
下面是調用程序:
//AppMain.cs using System; namespace NiwalkerDemo { public class AppMain { static void Main() { Order order1=new Order(100); order1.ProductName="Item1"; order1.Quantity=150; order1.Submit(); Order order2=new Order(101); order2.ProductName="Item2"; order2.Quantity=150; order2.Submit(); } } }
程序看上去還不錯,商務對象封裝了商務規則,運行的結果也符合要求。但是我好像聽到你在抱怨了,沒有嗎?當你的客戶的需求改變的時候(客戶總是經常改變他們的需求),比如庫存檢查的規則不是單一的檢查產品的數量,還要檢查產品是否被預訂的多種情況,那麽你需要改變Inventory的代碼,同時還要修改Order中的代碼,我們的例子只是一個簡單的商務邏輯,實際的情況比這個要復雜的多。問題在於Order對象同其他的對象之間是緊耦合的,從OOP的觀點出發,這樣的設計是有問題的,如果你寫出這樣的程序,至少不會在我的團隊裏面被Pass.
你說了:“No problem! 我們可以把商務邏輯抽出來放到一個專門設計的用來處理事務的對象中。”嗯,好主意,如果你是這麽想的,或許我還可以給你一個提議,使用Observer Design Pattern(觀察者設計模式):你可以使用delegate,在Order對象中定義一個BeforeSubmit和AfterSubmit事件,然後創建一個對象鏈表,將相關的對象插入到這個鏈表中,這樣就可以實現對Order提交事件的攔截,在Order提交之前和提交之後自動進行必要的事務處理。如果你感興趣的話,你可以自己動手來編寫這樣的一個代碼,或許還要考慮在分布式環境中(Order和Inventory不在一個地方)如何處理對象之間的交互問題。
幸運的是,.NET Framework中提供了實現這種技術的支持。在.NET Framework中的對象Remoting和組件服務中,有一個重要的攔截機制,在對象Remoting中,不同的應用程序之間的對象的交互需要穿越他們的域邊界,每一個應用域也可以細分為多個Context(上下文環境),每一個應用域也至少有一個默認的Context,即使在同一個應用域,也存在穿越不同Context的問題。NET的組件服務發展了COM+的組件服務,它使用Context Attribute來實現象COM+一樣的攔截功能。通過對調用對象的攔截,我們可以對一個方法的調用進行前處理和後處理,同時也解決了上述的跨越邊界的問題。
需要提醒你,如果你在MSDN文檔查ContextAttribute,我可以保證你得不到任何有助於了解ContextAttribute的資料,你看到的將是這麽一句話:“This type supports the .NET Framework infrastructure and is not intended to be used directly from your code.”——“本類型支持.NET Framework基礎結構,它不打算直接用於你的代碼。”不過,在msdn站點,你可以看到一些有關這方面的資料(見文章後面的參考鏈接)。
下面我們介紹有關的幾個類和一些概念,首先是:
ContextAttribute類
ContextAttribute派生自Attribute,同時它還實現了IContextAttribute和IContextProperty接口。所有自定義的ContextAttribute必須從這個類派生。
構造器:
ContextAttribute:構造器帶有一個參數,用來設置ContextAttribute的名稱。
公共屬性:
Name:只讀屬性。返回ContextAttribute的名稱
公共方法:
GetPropertiesForNewContext:虛擬方法。向新的Context添加屬性集合。
IsContextOK:虛擬方法。查詢客戶Context中是否存在指定的屬性。
IsNewContextOK:虛擬方法。默認返回true。一個對象可能存在多個Context,使用這個方法來檢查新的Context中屬性是否存在沖突。
Freeze:虛擬方法。該方法用來定位被創建的Context的最後位置。
ContextBoundObject類
實現被攔截的類,需要從ContextBoundObject類派生,這個類的對象通過Attribute來指定它所在Context,凡是進入該Context的調用都可以被攔截。該類從MarshalByRefObject派生。
以下是涉及到的接口:
IMessage:定義了被傳送的消息的實現。一個消息必須實現這個接口。
IMessageSink:定義了消息接收器的接口,一個消息接收器必須實現這個接口。
還有幾個接口,我們將在下一節結合攔截構架的實現原理中進行介紹。
(待續)
參考文章:Decouple Components by Injecting Custom Services into Your Object‘s Interception Chain
Attribute在.NET編程的應用(六)
(承上節) .NET Framework攔截機制的設計中,在客戶端和對象之間,存在著多種消息接收器,這些消息接收器組成一個鏈表,客戶端的調用對象的過程以及調用返回實行攔截,你可以定制自己的消息接收器,把它們插入了到鏈表中,來完成你對一個調用的前處理和後處理。那麽調用攔截是如何構架或者說如何實現的呢?
在.NET中有兩種調用,一種是跨應用域(App Domain),一種是跨上下文環境(Context),兩種調用均通過中間的代理(proxy),代理被分為兩個部分:透明代理和實際代理。透明代理暴露同對象一樣的公共入口點,當客戶調用透明代理的時候,透明代理把堆棧中的幀轉換為消息(上一節提到的實現IMessage接口的對象),消息中包含了方法名稱和參數等屬性集,然後把消息傳遞給實際代理,接下去分兩種情況:在跨應用域的情況下,實際代理使用一個格式化器對消息進行序列化,然後放入遠程通道中;在跨上下文環境的情況下,實際代理不必知道格式化器、通道和Context攔截器,它只需要在向前傳遞消息之前對調用實行攔截,然後它把消息傳遞給一個消息接收器(實現IMessageSink的對象),每一個接收器都知道自己的下一個接收器,當它們對消息進行處理之後(前處理),都將消息傳遞給下一個接收器,一直到鏈表的最後一個接收器,最後一個接收器被稱為堆棧創建器,它把消息還原為堆棧幀,然後調用對象,當調用方法結果返回的時候,堆棧創建器把結果轉換為消息,傳回給調用它的消息接收器,於是消息沿著原來的鏈表往回傳,每個鏈表上的消息接收器在回傳消息之前都對消息進行後處理。一直到鏈表的第一個接收器,第一個接收器把消息傳回給實際代理,實際代理把消息傳遞給透明代理,後者把消息放回到客戶端的堆棧中。從上面的描述我們看到穿越Context的消息不需要格式化,CLR使用一個內部的通道,叫做CrossContextChannel,這個對象也是一種消息接收器。
有幾種消息接收器的類型,一個調用攔截可以在服務器端進行也可以在客戶端進行,服務器端接收器攔截所有對服務器上下文環境中對象的調用,同時作一些前處理和後處理。客戶端的接收器攔截所有外出客戶端上下文環境的調用,同時也做一些前處理和後處理。服務器負責服務器端接收器的安裝,攔截對服務器端上下文環境訪問的接收器稱為服務器上下文環境接收器,那些攔截調用實際對象的接收器是對象接收器。通過客戶安裝的客戶端接收器稱為客戶端上下文環境接受器,通過對象安裝的客戶端接收器則稱為特使(Envoy)接收器,特使接收器僅攔截那些和它相關的對象。客戶端的最後一個接收器和服務器端的第一個接收器是CrossContextChannel類型的實例。不同類型的接收器組成不同的段,每個段的端點都裝上稱為終結器的接收器,終結器起著把本段的消息傳給下一個段的作用。在服務器上下文環境段的最後一個終結器是ServerContextTerminatorSink。如果你在終結器調用NextSink,它將返回一個null,它們的行為就像是死端頭,但是在它們內部保存有下一個接收器對象的私有字段。
我們大致介紹了.NET Framework的對象調用攔截的實現機制,目的是讓大家對這種機制有一個認識,現在是實現我們代碼的時候了,通過代碼的實現,你可以看到消息如何被處理的過程。首先是為我們的程序定義一個接收器CallTraceSink:
//TraceContext.cs using System; using System.Runtime.Remoting.Contexts; using System.Runtime.Remoting.Messaging; using System.Runtime.Remoting.Activation; namespace NiwalkerDemo { public class CallTraceSink : IMessageSink //實現IMessageSink { private IMessageSink nextSink; //保存下一個接收器 //在構造器中初始化下一個接收器 public CallTraceSink(IMessageSink next) { nextSink=next; } //必須實現的IMessageSink接口屬性 public IMessageSink NextSink { get { return nextSink; } } //實現IMessageSink的接口方法,當消息傳遞的時候,該方法被調用 public IMessage SyncProcessMessage(IMessage msg) { //攔截消息,做前處理 Preprocess(msg); //傳遞消息給下一個接收器 IMessage retMsg=nextSink.SyncProcessMessage(msg); //調用返回時進行攔截,並進行後處理 Postprocess(msg,retMsg); return retMsg; } //IMessageSink接口方法,用於異步處理,我們不實現異步處理,所以簡單返回null, //不管是同步還是異步,這個方法都需要定義 public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink) { return null; } //我們的前處理方法,用於檢查庫存,出於簡化的目的,我們把檢查庫存和發送郵件都寫在一起了, //在實際的實現中,可能也需要把Inventory對象綁定到一個上下文環境, //另外,可以將發送郵件設計為另外一個接收器,然後通過NextSink進行安裝 private void Preprocess(IMessage msg) { //檢查是否是方法調用,我們只攔截Order的Submit方法。 IMethodCallMessage call=msg as IMethodCallMessage; if(call==null) return; if(call.MethodName=="Submit") { string product=call.GetArg(0).ToString(); //獲取Submit方法的第一個參數 int qty=(int)call.GetArg(1); //獲取Submit方法的第二個參數 //調用Inventory檢查庫存存量 if(new Inventory().Checkout(product,qty)) Console.WriteLine("Order availible"); else { Console.WriteLine("Order unvailible"); SendEmail(); } } } //後處理方法,用於記錄訂單提交信息,同樣可以將記錄作為一個接收器 //我們在這裏處理,僅僅是為了演示 private void Postprocess(IMessage msg,IMessage retMsg) { IMethodCallMessage call=msg as IMethodCallMessage; if(call==null) return; Console.WriteLine("Log order information"); } private void SendEmail() { Console.WriteLine("Send email to manager"); } } ...
接下來我們定義上下文環境的屬性,上下文環境屬性必須根據你要創建的接收器類型來實現相應的接口,比如:如果創建的是服務器上下文環境接收器,那麽必須實現IContributeServerContextSink接口。
... public class CallTraceProperty : IContextProperty, IContributeObjectSink { public CallTraceProperty() { } //IContributeObjectSink的接口方法,實例化消息接收器 public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next) { return new CallTraceSink(next); } //IContextProperty接口方法,如果該方法返回ture,在新的上下文環境中激活對象 public bool IsNewContextOK(Context newCtx) { return true; } //IContextProperty接口方法,提供高級使用 public void Freeze(Context newCtx) { } //IContextProperty接口屬性 public string Name { get { return "OrderTrace";} } } ...
最後是ContextAttribute
... [AttributeUsage(AttributeTargets.Class)] public class CallTraceAttribute : ContextAttribute { public CallTraceAttribute():base("CallTrace") { } //重載ContextAttribute方法,創建一個上下文環境屬性 public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg) { ctorMsg.ContextProperties.Add(new CallTraceProperty()); } } }
為了看清楚調用Order對象的Submit方法如何被攔截,我們稍微修改一下Order類,同時把它設計為ContextBoundObject的派生類:
//Inventory.cs //Order.cs using System; namespace NiwalkerDemo { [CallTrace] public class Order : ContextBoundObject { ... public void Submit(string product, int quantity) { this.product=product; this.quantity=quantity; } ... } }
客戶端調用代碼:
... public class AppMain { static void Main() { Order order1=new Order(100); order1.Submit("Item1",150); Order order2=new Order(101); order2.Submit("Item2",150); } } ...
運行結果表明了我們對Order的Sbumit成功地進行了攔截。需要說明的是,這裏的代碼僅僅是作為對ContextAttribute應用的演示,它是粗線條的。在具體的實踐中,大家可以設計的更精妙。
後記:本來想對Attribute進行更多的介紹,發現要講的東西實在是太多了。請允許我在其他的專題中再來討論它們。十分感謝大家有耐心讀完這個系列。如果這裏介紹的內容在你的編程生涯有所啟迪的話,那麽就是我的莫大榮幸了。再一次謝謝大家。
(全文完)
(轉)Attribute在.net編程中的應用