前言
??????
近一段時間,對
AOP
思想進行了學習與研究,主要是看網上的一些資料,下面就這段時間的學習進行初步的總結,希望能和大家多多交流。
AOP
思想
?????? 1
、
AOP
思想的形成
??????
軟件設計因為引入面向對象思想而逐漸變得豐富起來。
“
一切皆為對象
”
的
精義,使得程序世界所要處理的邏輯簡化,開發者可以用一組對象以及這些對象之間的關系將軟件系統形象地表示出來。然而,面向對象設計的唯一問題是,它本質
是靜態的,封閉的,任何需求的細微變化都可能對開發進度造成重大影響??赡芙鉀Q該問題的方法是設計模式。然而鑒于對象封裝的特殊性,
“
設計模式
”
的觸角始終在接口與抽象中大做文章,而對于對象內部則無能為力。
Aspect-Oriented Programming
(面向方面編程,
AOP
)正好可以解決這一問題。它允許開發者動態地修改靜態的
OO
模型,構造出一個能夠不斷增長以滿足新增需求的系統。
AOP
利用一種稱為
“
橫切
”
的技術,剖解開封裝的對象內部,并將那些影響了多個類的行為封裝到一個可重用模塊,并將其名為
“Aspect”
,即方面。所謂
“
方面
”
,簡單地說,就是將那些與業務無關,卻為業務模塊所共同調用的邏輯或責任,例如事務處理、日志管理、權限控制等,封裝起來,便于減少系統的重復代碼,降低模塊間的耦合度,并有利于未來的可操作性和可維護性。
?????
總體來說:
OOP
提高了代碼的重用,設計模式解決了模塊之間的耦合,
AOP
解決某個模塊內部的變化問題。
2
、
AOP
技術本質
??????
AOP
把
軟件系統分為兩個部分:核心關注點和橫切關注點。所謂核心關注點是:業務處理的主要流程,也就是說這個解決方案要作的事。所謂橫切關注點是:與核心業務無
關的部分,它常常發生在核心關注點地多處,而各處基本相似,如:日志、權限等。橫切關注點雖然與核心的業務實現無關,但是它卻是一種。更
Common
的業務,個個橫切關注點離散的分布于核心業務地多處,導致系統中的每一個模塊都與這些業務具有很強的依賴性。橫切關注點所代表的業務,即為“方面(
Aspect
)”
AOP
的核心思想就是
“
將應用程序中的商業邏輯同對其提供支持的通用服務進行分離。
”
?????? AOP
的技術特性大體相同,分別是:
(
1
)
join point
(連接點):是程序執行中的一個精確執行點,例如類中的一個方法。它是一個抽象的概念,在實現
AOP
時,并不需要去定義一個
join point
。
(
2
)
point cut
(切入點):本質上是一個捕獲連接點的結構。在
AOP
中,可以定義一個
point cut
,來捕獲相關方法的調用。
(
3
)
advice
(通知):是
point cut
的執行代碼,是執行
“
方面
”
的具體邏輯。
(
4
)
aspect
(方面):
point cut
和
advice
結合起來就是
aspect
,它類似于
OOP
中定義的一個類,但它代表的更多是對象間橫向的關系。
(
5
)
introduce
(引入):為對象引入附加的方法或屬性,從而達到修改對象結構的目的。有的
AOP
工具又將其稱為
mixin
。
.Net
中實現
AOP
所需的基礎知識
??????
學習
AOP
的實現之前需要對一些知識進行了解:元數據(
Metadata
),可執行可移植文件(
PE
文件),上下文(
Context
)等基礎知識。
1、?
元數據:一種二進制信息,用以對存儲在
CLR
中可移植可執行文件或存儲在內存中的程序進行描述。說得形象點:大家可以把它想象為一個列表,列表中的內容是對程序或模塊中的類以及類中的函數。
2、?
可執行可移植文件(
PE
文件):在編譯后,程序會被編譯成
PE
文件,其實就是我們所見的
dll
和
exe
文件,在編譯的時候,通過
/target
命令行開關(快捷形式
/t
)選擇目標文件的種類,如:
/t:exe
編譯成控制臺程序,
/t:winexe
編譯成
WinForm
程序,
/t:library
編譯成
dll
程序,
/t:module
生成的文件擴展名為
.netmodule
PE
文件分為三個部分:
PE
標頭、元數據、
MSIL
指令
PE
標頭:
PE
文件主要部分的索引和入口點的地址
MSIL
指令:組成代碼的中間語言指令。其中可能帶有元數據標記
3、?
上下文:是指一個邏輯上的執行環境。每一個應用程序域(
AppDomain
)都有一個或多個
Context
。
.Net
中所有對象都會在相應的
Context
中創建和運行。
Context
提供了錯誤傳播、事務管理和同步功能,而對象的創建和運行就存在于該
Context
中。
4、
Attribute
:是被指定給某一聲明的一則附加的聲明性信息,利用
Attribute
我們可以擴展元數據。自定義
Attribute
實際上就是寫一個繼承自
Attribute
的類,當把一個
Attribute
對象施加到某個程序元素上(類,方法
…
)這個對象的實例化發生在編譯時。
5、?
代理:
.Net
中的偵聽器是兩個對象的組合,分為
Transparent Proxy
和
Real Proxy
。
Transparent Proxy
的行為與目標對象相同,它將調用堆棧序列化為一個消息對象,然后再將消息傳遞給
Real Proxy
。
Real Proxy
接受消息,并發給第一個消息接受處理。
Transparent Proxy
和
Real Proxy
在上下文中會起到偵聽器的作用,如下圖:
6
、.Net對象方法調用機制:首先,一個類的對象被實例化,系統會分配給對象一個邏輯的上下文環境(Context)。當調用對象方法時,Transparent Proxy會將調用堆棧的幀序列化一個IMessage對象,然后傳給Real Proxy,Real Proxy會判斷這個IMessage是要跨越應用程序域還是跨越上下文環境(當然,這里討論的是第二種情況),在跨越上下文環境的情況下,Real Proxy將消息傳個第一個消息接收器。第一個消息接收器在接到IMessage后調用它的SyncProcessMessage方法對這個IMessage對象進行處理(前處理)。處理后傳給下一個消息接收器…只到傳給最后一個消息接收器(堆棧構建器),堆棧構建器把消息還原為堆棧幀,然后調用對象,當調用方法結果返回的時候,堆棧構建器把結果轉換為IMessage對象,傳回給調用它的消息接收器,于是,消息沿著原來的鏈表往回傳,每個消息接收器再對IMessage對象進行處理(后處理)。直到鏈表的第一個接收器,第一個接收器把IMessage對象傳回Real Proxy,Real Proxy把消息傳給Transparent Proxy,Transparent Proxy把IMessage對象放回客戶端的堆棧中。
代理創建屬性的過程如下圖所示,其中先創建上下文屬性,然后創建消息接收。
.NET MessageSink
的創建
.Net
中實現
AOP
1、?
AOP
的實現思想
在了解了上面的知識和
AOP
的本質后,來看看
AOP
的實現思想。為了提供
AOP
的功能,需要在每個方法建立執行組建所必需的環境的前后訪問調用堆棧。這就需要一個偵聽器,以及組件的上下文
(
1
)為對象創建指定的上下文環境
.Net
中偵聽的關鍵在于要為組件提供上下文,利用到此對象的上下文,我們在實例化一個對象時,系統會為此對象創建一個默認的上下文運行環境,如果要獲取這個對象的上下文,就要讓系統為對象創建一個指定的上下文環境。就要在這個對象的類聲明時讓此類繼承
ContextBoundObject
類型。這樣當客戶代碼在實例化對象時,系統就會為對象創建一個指定的上下文環境。
(
2
)將自定義屬性(
Attribute
)和目標對象上下文聯系
對于自定義的屬性,需要繼承
ContextAttribute
類型,這個類型是一個特殊的
Attribute,
通過它,
可以獲得對象需要的合適的上下文。在這個類的實現中要通過重寫
GetPropertiesForNewContext
方法的實現將自定義的上下文屬性(Property)加入到對象的調用請求中
(
3
)定義上下文屬性(
Property
)
作為方面消息接收工廠,定義此類時要實現
IContextProperty
和IContributeObjectSink,用來將所提供的服務器對象的消息接收器(IMessageSink對象)連接到給定的接收鏈前面。
IContextProperty
的對象可以為相關的Context提供一些屬性,從上下文屬性收集命名信息,并確定新上下文是否與上下文屬性兼容。
IContributeObjectSink
:在遠程處理調用的服務器端分配對象特定的偵聽接收器
(
4
)定義消息接收器(
IMessageSink
)
此類要繼承
IMessageSink
接口,這個類應該說是要植入的方面,代理通過它進行消息的傳遞,并獲取方法間傳遞的消息。
2、?
具體實現
??????
在這里以
Web
程序為例,建立一個應用場景:對數據庫中的數據操作記錄日志:
??????
首先,我們先來建立一個表
T_Test
,向此表中添加數據
CREATE TABLE [dbo].[T_Test] (
?????? [ID] [int] IDENTITY (1, 1) NOT NULL ,
?????? [Test] [nvarchar] (50) COLLATE Chinese_PRC_CI_AS NULL
) ON [PRIMARY]
??????
建立一個記錄日志的表
T_LogRecord
CREATE TABLE [dbo].[T_LogRecord] (
?????? [ID] [int] IDENTITY (1, 1) NOT NULL ,
?????? [OptionString] [nvarchar] (4000) COLLATE Chinese_PRC_CI_AS NULL
) ON [PRIMARY]
??????
對于
T_Test
表的添查刪改不再過多贅述,程序實現時只是將數據的讀取操作的方法提取出來做成一個
PubFunc
類,這個類是上面所說的核心關注點,代碼如下:
public
class PubFunc
??? {
???????
private
string GetConnectString()
??????? {
???????????
string strDBConn = "";
???????????
try
??????????? {
??????????????? strDBConn = ConfigurationSettings.AppSettings["DBConn"].ToString();
??????????? }
???????????
catch
??????????? {
???????????????
if(strDBConn == null || strDBConn == "")
??????????????????? strDBConn = "workstation
id='ESINT-TZEO00YCX';packet size=4096;user id=sa;data
source='192.168.0.89';persist security info=False;initial catalog=Test";
??????????? }
???????????
return strDBConn;
??????? }
???????
public DataTable ReadFunc(string strSql)
??????? {
???????????
try
??????????? {
??????????????? SqlConnection conn = new SqlConnection(GetConnectString());
??????????????? conn.Open();
??????????????? SqlDataAdapter adapter = new SqlDataAdapter(strSql,conn);
??????????????? DataTable dt = new DataTable();
??????????????? adapter.Fill(dt);
??????????????? conn.Close();
???????????????
return dt;
??????????? }
???????????
catch(Exception ex)
??????????? {
???????????????
throw ex;
??????????? }
??????? }
???????
public
bool OptionFunc(string strSql)
??????? {
???????????
bool Flag = false;
??????????? SqlConnection conn = new SqlConnection(GetConnectString());
??????????? conn.Open();
???????????
try
??????????? {
??????????????? SqlCommand com = new SqlCommand(strSql,conn);
??????????????? com.ExecuteNonQuery();
??????????????? Flag = true;
??????????? }
???????????
catch(Exception ex)
??????????? {
???????????????
throw ex;
??????????? }
???????????
finally
??????????? {
??????????????? conn.Close();
??????????? }
???????????
return Flag;
??????? }
??? }
???
其中,要記錄對表的操作,我們就要截獲OptionFunc方法的調用。那么首先要獲得對象的上下文,就要讓PubFunc類繼承ContextBoundObject類
???
public
class PubFunc:ContextBoundObject
???
這樣就可以獲得到PubFunc類的上下文,然后要為PubFunc類植入方面,就要擴展PubFunc類的元數據,所以要有一個自定義的Attribute:
??? [AttributeUsage(AttributeTargets.Class)]
???
public
class LogAttribute:ContextAttribute
??? {
???????
public LogAttribute():base("Log")
??????? {
???????????
??????? }
??? }
自定義屬性有命名規范,一般是“自定義屬性名”+ “Attribute”,例如,定義一個Log屬性,則自定義屬性類的名字就是:LogAttribute,當這個屬性被植入時,通常這樣寫:
[Log]
public class PubFunc
{
??? //
…
}
系統在運行時會先根據所寫的屬性去找,當找不到時回去查找所寫的屬性加上Attribute,如上面的代碼:系統會先去找Log屬性,當找不到Log屬性后,系統會去找LogAttribute。
定義LogAttribute類后,我們還要重寫ContextAttribute類中的GetPropertiesForNewContext方法。這個方法主要作用是將我們編寫的自定義上下文屬性(Property)加入IConstructionCallMessage(對象的結構調用請求)中的ContextProperties屬性列表中。
public
override
void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
??????? {
??????????? ctorMsg.ContextProperties.Add(new LogProperty());
??? }
擴展了對象的元數據后,就要為對象加入自定義的上下文屬性(Property),此類要繼承IContextProperty,IContributeObjectSink
public
class LogProperty:IContextProperty,IContributeObjectSink
接口IContextProperties:
屬性Name表示ContextProperty的名字,要求在整個Context中必須唯一,(這里,做了一下實驗,把兩個ContextProperty的Name都置為“LogAOP”,結果只有一個方面被執行)。
IsNewContextOK
確認Context是否存在沖突
Freeze
通知ContextProperty,當新的Context構造完成時,則進入Freeze狀態
注意:此接口只能為Context提供基本屬性,并不能完成對方法調入消息的截取。所以還要繼承一個IContributeObjectSink,用來實現偵聽器
接口IContributeObjectSink:在遠端處理調用的服務器端分配對象特定的偵聽接收器
GetObjectSink
將所提供的服務器對象的消息接收器連接到給定的接收器鏈前面,其中
nextSink
參數是到目前為止所形成的接收鏈,換句話說:可以將自定義的消息接收器(IMessageSink對象)連接到接收鏈的前面。
public
class LogProperty:IContextProperty,IContributeObjectSink
??? {
???????
public LogProperty()
??????? {
???????????
??????? }
??????? #region
IContextProperty
成員
???????
public
string Name
??????? {
???????????
get
??????????? {
???????????????
return
"Log";
??????????? }
??????? }
???????
public
bool IsNewContextOK(Context newCtx)
??????? {
???????????
return
true;
??????? }
???????
public
void Freeze(Context newContext)
??????? {
???????????
return;
??????? }
??????? #endregion
??????? #region
IContributeObjectSink
成員
???????
public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
??????? {
???????????
return
new LogSink(nextSink);
??????? }
??????? #endregion
}
有了ContextProperty,還要為它提供一個消息接收器(IMessageSink),也就是要編輯所需的方面,定一個LogSink類型,要繼承IMessageSink接口
public
class LogSink:IMessageSink
接口IMessageSink:定義消息接收器的接口
SyncProcessMessage
:同步操作處理給定消息,在此方法中可以加入方面的執行,
AsyncProcessMessage
:異步操作
IMessageSink NextSink
:獲取接收器鏈中的下一個消息接收器。將多個MessageSink連接起來,以形成一個消息接收鏈。
將要植入的方面定義為Before_Log函數,用來記錄操作日志。對于一個對象方法的調用在
.Net
中實現
AOP
所需的基礎知識
中已提到,分為兩個狀態,前處理和后處理??梢栽谇疤幚碇屑尤雽嘞薜呐袛啵诤筇幚碇锌梢约尤雽θ罩恋挠涗洝T谶@里舉一個簡單的例子,在前處理的時候,加入日志的記錄。
在這里說一下我的理解:我理解前處理和后處理實際上在
nextSink
調用
SyncProcessMessage
處理消息
IMessage
對象的前后
public
class LogSink:IMessageSink
??? {
???????
private IMessageSink _nextSink;
???????
public LogSink(IMessageSink nextSink)
??????? {
??????????? _nextSink = nextSink;
??????? }
??????? #region
IMessageSink
成員
???????
public IMessage SyncProcessMessage(IMessage msg)
??????? {
??????????? IMethodCallMessage call = msg as IMethodCallMessage;
???????????
if(call.MethodName == "OptionFunc")
??????????????? Before_Log(call);
??????????? IMethodReturnMessage reply = (IMethodReturnMessage)_nextSink.SyncProcessMessage(msg);
???????????
return reply;
??????? }
???????
public IMessageSink NextSink
??????? {
???????????
get
??????????? {
???????????????
return _nextSink;
??????????? }
??????? }
???????
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
??????? {
???????????
return
null;
??????? }
??????? #endregion
???????
private
void Before_Log(IMethodCallMessage msg)
??????? {
???????????
if(msg == null)
??????????? {
???????????????
return;
??????????? }
???????????
string strSql = "Insert Into T_LogRecord (OptionString) Values (@OptionString)";
??????????? SqlConnection conn = new SqlConnection("workstation
id='ESINT-TZEO00YCX';packet size=4096;user id=sa;data
source='192.168.0.89';persist security info=False;initial catalog=Test");
??????????? conn.Open();
???????????
try
??????????? {
??????????????? SqlCommand com = new SqlCommand(strSql,conn);
???
??????????? com.Parameters.Add("@OptionString",msg.Args[0]);
??????????????? com.ExecuteNonQuery();
??????????? }
???????????
catch(Exception ex)
??????????? {
???????????????
throw ex;
??????????? }
???????????
finally
??????????? {
??????????????? conn.Close();
??????????? }
??????? }
}
這樣,就完成了方面的植入,最后在核心關注點中施加上屬性就可以了
[Log]
public
class PubFunc:ContextBoundObject
當客戶端類實例化類并調用一個方法時,方面就被激活了:
?????????????
Common.PubFunc func = new Common.PubFunc();
???????????
string strSql = "";
???????????
if(Request.QueryString["Func"].ToString() == "Add")
??????????????? strSql = "Insert Into T_Test (Test) Values ('" + this.wtxtTest.Text + "')";
???????????
else
??????????????? strSql = "Update T_Test Set Test = '" + this.wtxtTest.Text + "' Where ID = '" + Request.QueryString["ID"].ToString() + "'";
???????????
if(func.OptionFunc(strSql))
??????????????? Response.Redirect("WebForm1.aspx");
???????????
else