文章开头我们需要了解几个概念,如:什么是面向切面编程?它的发展及其优势是什么?首先我们来共同了解下什么面向切面编程,又称作AOP。
AOP(Aspect-Oriented Programming)是对OOP的一种补充,它从一个不同于OOP的角度来看待程序的结构:OOP将应用程序分解为一系列表现为继承关系的对象;AOP则把程序分解为一系列方面(aspects)或者关注点(concerns)。AOP将诸如事务管理等本来横向分布在多个对象中的关注点进行了模块化处理(这些关注点也常称为横切(crosscutting)关注点)。
AOP框架是Spring.NET的一个关键组件。Spring.NET的IoC容器与AOP框架是相互独立的,两者完全可以不依赖对方而单独使用,但是AOP做为一个强大的中间件解决方案,完善了IoC容器的功能。
接下来,我们需要了解下,AOP编程中的几个概念:
-
方面(Aspect):对横向分布在多个对象中的关注点所做的模块化。在企业应用中,事务管理就是一个典型的横切关注点。Spring.NET将方面实现为Advisor或拦截器(interceptor)。(按:Advisor是通知和切入点的组合,拦截器实际就是指通知,注意在本文档中,一般会把环绕通知称为拦截器,而将其它类型的通知称为通知,这是因为环绕通知实现的是AopAlliance.Intercept.IMethodInterceptor接口,而其它通知类型实现的都是Spring.Aop命名空间下的通知接口。)
-
连接点(Joinpoint):程序执行过程中的一个点,例如对某个方法的调用或者某个特定异常的抛出都可以称为连接点。
-
通知(Advice):AOP框架在某个连接点所采取的行为。通知有多种类型,包括“环绕”通知,“前置”通知和“异常”通知等,后文将对通知类型进行讨论。包括Spring.NET在内的很多AOP框架都把通知建模为拦截器(interceptor),并且会维护一个"包围"在连接点周围的拦截器链。
-
切入点(Pointcut):指通知的应用条件,用于确定某个通知要被应用到哪些连接点上。AOP框架应允许让开发人员指定切入点,例如,可以使用正则表达式来指定一个切入点。
-
引入(Introduction):向目标对象添加方法或字段的行为。Spring.NET允许为任何目标对象引入新的接口。例如,可以利用引入让任何对象在运行期实现IAuditable接口,以简化对象状态变化的跟踪过程。(按:也称为mixin,混入)
-
目标对象(Target object):指包含连接点的对象。也称为被通知或被代理对象。(按:“被通知对象”实际是“被应用了通知的对象”,在译文中,将advised object或proxied object统称为目标对象,这样更为统一)
-
AOP代理(AOP proxy):由AOP框架在将通知应用于目标对象后创建的对象。在Spring.NET中,AOP代理是使用IL代码在运行时创建的动态代理。
-
织入(Weaving):将方面进行组装,以创建一个目标对象。织入可以在编译期完成(例如使用Gripper_Loom.NET编译器),也可以在运行时完成。Spring.NET在运行时执行织入。
-
各种通知类型包括:
-
环绕通知(Around Advise):包围(按:即在连接点执行的前、后执行)某个连接点(如方法调用)的通知。这是功能最强大的一种通知。环绕通知允许在方法调用的前后执行自定义行为。它可以决定是让连接点继续执行,还是用自己的返回值或异常来将连接点“短路”。
-
前置通知(Before Advise):在某个连接点执行之前执行,但是不具备阻止连接点继续执行的能力(除非它抛出异常)。
-
异常通知(Throws Advise):当方法(连接点)抛出异常时执行。Spring.NET的异常通知是强类型的(按:Spring.NET用标识接口来定义异常通知,异常通知的处理方法仅需遵循一定的命名规则,可以用具体的异常类型声明其参数,参见12.3.2.3节),所以,可以在代码中直接捕捉某个类型的异常(及其子类异常),不必从Exception转型。
-
后置通知(After returning Advise):在连接点正常执行完成后执行,例如,如果方法正常返回,没有抛出异常时,后置通知就会被执行。
Spring.NET内置了以上所有类型的通知。在应用时,应尽量使用功能最少(只要对要实现的行为来说是足够的)的通知类型,这样可简化编程模型并减少出错的可能。例如,如果只需使用某个方法的返回值来更新缓存,那么用后置通知就比环绕通知合适。因为,尽管环绕通知可以完成同样的功能,但在后置通知中不用象环绕通知那样必须调用IMethodInvocation接口的Proceed()方法来允许连接点继续执行,所以连接点总是能正常执行。(按:换句话说,因为环绕通知可以控制连接点的继续执行,所以如果没有调用IMethodInvocation接口的Proceed()方法,连接点就会被“短路”;而使用后置通知就不存在这个问题)。
切入点是AOP的关键概念,使AOP从根本上区别于旧的拦截技术。切入点使通知可以独立于OO的继承层次之外。例如,一个声明式事务处理的环绕通知可以应用于不同对象的方法。切入点是AOP的结构性要素。
Spring.NET AOP的功能
Spring.NET的AOP框架完全用C#实现。所有的织入工作都在运行时完成,不需要特殊的编译过程。由于在不必控制或修改程序集装载的方式,也不依赖于非托管API,所以Spring.NET的AOP框架适用于任何CLR环境。
目前,Spring.NET支持对方法调用的拦截。虽然也可以在不破坏核心API的前提下加入对字段拦截的支持,但Spring.NET没有实现这一功能。这源于一个有争议的话题:字段拦截破坏了OO的封装性。在应用开发中这并不是明智之举。
Spring.NET为切入点和不同的通知类型都提供了相应的类。在Spring.NET的类库中,方面由Advisor对象来表示,而Advisor又由通知和切入点组成(切入点用于确定将通知应用在哪些连接点上)。
各种类型的通知分别是由IMethodInterceptor接口(由AOP联盟定义的拦截器API,在AopAlliance.Intercept命名空间下)和Spring.Aop命名空间下的各个通知接口定义的。所有通知都必须(最终)实现AopAlliance.Aop.IAdvice接口(这是个标识接口,没有定义任何成员),Spring.Aop命名空间下的IMethodInterceptor、IThrowsAdvice、IBeforeAdvice和IAfterReturningAdvice接口都扩展了IAdvice接口。我们将在后文中讨论它们。
Spring.NET将AOP联盟定义的java接口移植到了.NET平台上。环绕通知必须实现AOP联盟的AopAlliance.Intercept.IMethodInterceptor接口。在Java领域,AOP联盟的接口已经得到了广泛的应用,但在.NET领域,Spring.NET是目前唯一实现了这些接口的AOP框架。就短期来说,这为同时使用.NET和Java的开发人员提供了一个统一的编程模型;就长期而言,我们也希望看到更多的.NET项目能采用AOP联盟的接口。
Spring.NET的AOP框架并不是要象AspectJ(Aspect#)一样全面支持AOP。但Spring.NET的AOP框架可以为.NET应用领域中许多应该用AOP来处理的问题提供很好的解决方案。
一般我们会将Spring.NET的AOP框架与IoC容器一起使用。在IoC容器中,可以象普通对象一样来布署AOP通知(也可以使用强大的自动代理(autoproxying)功能);通知和切入点都可以由Spring.NET的IoC容器管理
Spring.NET的AOP代理
Spring.NET利用System.Reflection.Emit命名空间下的类在运行时动态创建IL代码来生成AOP代理。这使得代理(的创建)非常高效,并且不受任何继承层次的限制。
在.NET中,实现AOP代理的另一种方法是使用ContextBoundObject类和Remoting基础框架。但这个方法不是太好,因为它要求被代理的类必须直接或间接继承自ContextBoundObject。这个不必要的限制会影响到对象模型的设计,也无法将AOP应用在设计者无法直接控制的“第三方”类型上。同时,由于上下文切换和Remoting基础框架的开销,所生成的代理也会比IL直接生成的代理慢得多。
Spring.NET的AOP代理是非常“智能”的。在生成代理时,由于代理的配置是能确定的,所以生成的代理对象仅会在必要时才通过反射调用目标方法(即,当已有通知应用到目标方法时)。其他情况下,目标方法是直接调用的,避免了因反射调用而带来的性能开销。
最后,Spring.NET的AOP代理永远不直接返回目标对象的原始引用。如果检测到目标对象的某个方法返回了自身的引用(比如用return this;),AOP代理会将返回值替换为代理自身的引用。
目前,AOP代理生成器使用对象的组合来转发从代理到目标对象的调用,这和传统Decorator模式的实现方式很相似。这表示被代理的类必须实现一或多个接口,让类实现接口并不是限制,而是在任何时候都应该遵守的最佳编程方式。同时,这样也会比让类继承ContextBoundObject更少侵入性,
未来的版本会使用继承来实现代理,这样就可以代理没有实现任何接口的类,同时也可以消除一些用组合方式无法解决的遗留问题。
-
通过对以上概念的了解,接下来,我们需要通过实战代码,来进一步阐述AOP。首先简单配置下Spring.NET框架吧。我们可以通过Nuget,来进行对Spring.NET的安装引用,这里,根据需求,我们需要引用Spring.Core核心框。
rready工作完成后,接下来,继续配置,在应用程序配置文件中,配置如下:
<configuration>
<configSections>
<!--Spring.NET-->
<sectionGroup name="spring">
<section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core"/>
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/>
</sectionGroup>
<!--日志-->
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</sectionGroup>
</configSections>
下面在在App.config中进行配置的,我们也可以外部文件进行配置。
<spring>
<context>
<!--容器配置-->
<!--<resource uri="assembly://SpringApp/xmls/objects.xml"/>-->
</context>
</spring>
以下示例,是通过控制台程序进行演示的。首先,我们新建了一个objects.xml文件,用于配置应用程序中的对象.
<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net"
xmlns:aop="http://www.springframework.net/aop">
<!--Target 定义老师-->
<object id="teacher" type="SpringApp.Teacher,SpringApp"></object>
<!--定义代理对象-->
<object id="user" type="SpringApp.UserProxy,SpringApp"></object>
<object id="log" type="SpringApp.LogProxy,SpringApp"></object>
<!--定义通知-->
<object id="advisor" type="SpringApp.MyAdvisor,SpringApp">
<property name="user" ref="user"></property>
</object>
<object id="logAdvisor" type="SpringApp.LogAdvisor,SpringApp">
<property name="Log" ref="log"></property>
</object>
<!--定义切入点-->
<object id="pointcut" type="Spring.Aop.Support.SdkRegularExpressionMethodPointcut, Spring.Aop">
<property name="pattern" value="SpringApp.Teacher.Speak"/>
</object>
<!--切面-->
<aop:config>
<aop:advisor pointcut-ref="pointcut" advice-ref="advisor"/>
<aop:advisor pointcut-ref="pointcut" advice-ref="logAdvisor"/>
</aop:config>
</objects>
切入点,必须通过接口实现,因此,我们首先创建了一个名为ITeacher的接口。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
/******************************************************************************************************************
*
*
* 说 明:ITeacher (版本:Version1.0.0)
* 作 者:李朝强
* 日 期:2015/05/19
* 修 改:
* 参 考:http://my.oschina.net/lichaoqiang/
* 备 注:暂无...
*
*
* ***************************************************************************************************************/
namespace SpringApp
{
/// <summary>
/// 切入点:接口
/// </summary>
public interface ITeacher
{
/// <summary>
/// 定义方法:连接点
/// </summary>
void Speak();
/// <summary>
///
/// </summary>
void Run();
}
}
定义目标对象:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
/******************************************************************************************************************
*
*
* 说 明:Teacher对象(Target) (版本:Version1.0.0)
* 作 者:李朝强
* 日 期:2015/05/19
* 修 改:
* 参 考:http://my.oschina.net/lichaoqiang/
* 备 注:暂无...
*
*
* ***************************************************************************************************************/
namespace SpringApp
{
/// <summary>
/// Target对象: 教师
/// </summary>
public class Teacher : ITeacher
{
#region ITeacher 成员
/// <summary>
/// 被代理的方法必须通过接口实现 实现ITeacher接口 ============================================================= 切入点
/// </summary>
public void Speak()
{
//
Console.WriteLine("I'm a teacher!");
}
/// <summary>
///
/// </summary>
public void Run()
{
Console.WriteLine("Run:方法");
}
#endregion
}
}
截止目前,实现切入点的接口、Target都有了,接下来,该定义通知:
using AopAlliance.Intercept;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
/******************************************************************************************************************
*
*
* 说 明:通知者(版本:Version1.0.0)
* 作 者:李朝强
* 日 期:2015/05/19
* 修 改:
* 参 考:http://my.oschina.net/lichaoqiang/
* 备 注:暂无...
*
*
* ***************************************************************************************************************/
namespace SpringApp
{
/// <summary>
/// 1、通知者Advice:通知描述了切面要完成的任务,同时还描述了何时执行这个任务。
/// </summary>
public class MyAdvisor : IMethodInterceptor/*环绕通知*/
{
#region IMethodInterceptor 成员
/// <summary>
/// 要代理方法的对象
/// </summary>
public UserProxy user { set; get; }
/// <summary>
///
/// </summary>
/// <param name="invocation"></param>
/// <returns></returns>
public object Invoke(IMethodInvocation invocation)
{
//Target:
ITeacher teacher = (Teacher)invocation.Target;
//前置通知
user.ComeBefore(teacher);
object result = invocation.Proceed();
//后置通知
user.ComeAfter(teacher);
return result;
}
#endregion
}
/// <summary>
/// 2、日志
/// </summary>
public class LogAdvisor : IMethodInterceptor
{
#region IMethodInterceptor 成员
/// <summary>
///
/// </summary>
public LogProxy Log { get; set; }
/// <summary>
///
/// </summary>
/// <param name="invocation"></param>
/// <returns></returns>
public object Invoke(IMethodInvocation invocation)
{
object result = invocation.Proceed();
if (Log != null) { Log.Info("后置的方式:日志"); }
Console.WriteLine("通知日志");
return result;
}
#endregion
}
public class AuthenAdvisor : IMethodInterceptor
{
#region IMethodInterceptor 成员
/// <summary>
///
/// </summary>
/// <param name="invocation"></param>
/// <returns></returns>
public object Invoke(IMethodInvocation invocation)
{
throw new NotImplementedException();
}
#endregion
}
}
以下代码只是辅助用的:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
/******************************************************************************************************************
*
*
* 说 明: 代理(版本:Version1.0.0)
* 作 者:李朝强
* 日 期:2015/05/19
* 修 改:
* 参 考:http://my.oschina.net/lichaoqiang/
* 备 注:暂无...
*
*
* ***************************************************************************************************************/
namespace SpringApp
{
public class UserProxy
{
/// <summary>
/// id
/// </summary>
public long Id { get; set; }
/// <summary>
/// name
/// </summary>
public string Name { get; set; }
/// <summary>
///
/// </summary>
/// <param name="teacher"></param>
public void ComeBefore(ITeacher teacher)
{
Console.WriteLine("ComeBefore:通知前置");
}
/// <summary>
///
/// </summary>
/// <param name="teacher"></param>
public void ComeAfter(ITeacher teacher) { Console.WriteLine("ComeBefore:通知后置"); }
}
}
我们来通过Spring.NET框架,进行演示:
using System;
using Spring.Context;
using Spring.Context.Support;
using Spring.Globalization;
using Spring.Objects.Factory;
using System.IO;
using System.Reflection;
using Spring.Aop.Framework;
/******************************************************************************************************************
*
*
* 说 明: Spring.NET(版本:Version1.0.0)
* 作 者:李朝强
* 日 期:2015/05/19
* 修 改:
* 参 考:http://my.oschina.net/lichaoqiang/
* 备 注:暂无...
*
*
* ***************************************************************************************************************/
namespace SpringApp
{
internal class Program
{
/// <summary>
///
/// </summary>
/// <param name="args"></param>
private static void Main(string[] args)
{
Console.WriteLine("*****************Spring.NET**************************");
//<!--嵌入程序集方式,assembly://程序集名/项目名/objects.xml,更改属性,始终复制,生成操作,嵌入的资源-->
var xmls = new string[1] {/*"xmls/objects.xml"*/"assembly://SpringApp/SpringApp.config/objects.xml" };
//应用程序容器
IApplicationContext context = new XmlApplicationContext(xmls);
IObjectFactory factory = context;
//1、AOP
//Aop();
//2、代理
Proxy(factory);
Console.ReadLine();
}
/// <summary>
///
/// </summary>
static void Proxy(IObjectFactory factory)
{
//使用代理
ProxyFactory proxyFactory = new ProxyFactory(new Teacher());
proxyFactory.AddAdvice(new LogAdvisor());
ITeacher proxy = (ITeacher)proxyFactory.GetProxy();
proxy.Speak();
}
/// <summary>
///
/// </summary>
/// <param name="factory"></param>
static void Aop(IObjectFactory factory)
{
//实例化对象
ITeacher teacher = factory.GetObject("teacher") as ITeacher;
teacher.Speak();
//
teacher.Run();
}
}
}
关于演示代码:http://www.oschina.net/code/snippet_584165_53274