IOC&AOP学习和探索(一)——AOP基本理论及实现
前言
近一段时间,对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>");