前言
近一段时间,对
AOP
思想进行了学习与研究,主要是看网上的一些资料,下面就这段时间的学习进行初步的总结,希望能和大家多多交流。
AOP
思想
1
、
AOP
思想的形成
软件设计因为引入面向对象思想而逐渐变得丰富起来。
“
一切皆为对象
”
的
精义,使得程序世界所要处理的逻辑简化,开发者可以用一组对象以及这些对象之间的关系将软件系统形象地表示出来。然而,面向对象设计的唯一问题是,它本质
是静态的,封闭的,任何需求的细微变化都可能对开发进度造成重大影响。可能解决该问题的方法是设计模式。然而鉴于对象封装的特殊性,
“
设计模式
”
的触角始终在接口与抽象中大做文章,而对于对象内部则无能为力。
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
所需的基础知识
中已提到,分为两个状态,前处理和后处理。可以在前处理中加入对权限的判断,在后处理中可以加入对日至的记录。在这里举一个简单的例子,在前处理的时候,加入日志的记录。
在这里说一下我的理解:我理解前处理和后处理实际上在
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
Page.RegisterStartupScript("","<script language=\"javascript\">window.alert('fail done')</script>");