本文将对AOP拦截在Byteart Retail中的应用进行分析和介绍,同时会介绍AOP两个应用的具体实现方式,即异常处理与缓存机制的实现。
背景
就一个企业级应用程序而言,实现它的主要目的就是为了解决企业生产过程中出现的实际问题,比如数据问题、管理问题等。因此,应用程序的核心部分就应该是与企业业务相关的部分,也就是我们平时经常提到的“领域模型”。在进行领域模型的建模过程中,根据领域驱动的经验,需要排除所有与业务无关的成分,以便能够让模型能够完善地表达通用语言的描述,这对于系统分析和团队合作起着非常重要的作用。所以,在实际的系统设计中,我们都用所谓的POCO或POJO来表述领域模型对象及其之间的关系。所谓POCO/POJO,它们并不是不包含方法(Method)的对象,而相反的是,这些对象不仅包含方法的定义,而且这些方法都是用于处理业务逻辑的,由这些对象类型组成的领域模型称之为“富领域模型”。与系统中的其它对象的不同之处在于,它们不会“关心”任何与技术相关的东西,比如,它们不知道自己会被以一种什么样的方式持久化,不知道什么是日志、什么是缓存,如何记录日志、如何从缓存中获得需要的其它对象等等。在领域模型中使用POCO/POJO的目的只有一个:领域驱动。
基于这样的背景,既然领域模型需要维持其“纯净度”,那么对于整个应用程序而言,就需要采用一些框架方面的技术来支持这种需求。AOP拦截就是一种很好的方式(另一种是领域事件,我将会在“领域建模”相关的专题中介绍),通过AOP技术,可以动态产生所拦截类型的代理类型,并在代理类型中对类型操作的输入、输出进行一些定制化处理(比如日志记录等),由此,被代理的类型只需要关注它所需要处理的问题域(也就是我们平时所说的“业务”)即可,对于那些与技术相关的操作,就可以交给代理类的相关方法完成。
这样做的最大好处,就是关注点分离(Separation of Concerns),也就是上面所说的“被代理类只需要关心问题域即可”,其次就是技术相关处理的灵活性,比如我们可以选择采用或者不采用日志记录功能,而无需重新编译代码;我们可以动态地选择缓存解决方案,同样也无需重新编译代码。此外,AOP的采用对于领域模型的单体测试也带来了很多好处。
Byteart Retail案例中AOP的运用
Byteart Retail案例采用了Microsoft Patterns & Practices Unity作为AOP框架,展示了基于AOP的异常处理和缓存的应用。下面首先让我们一起看一下在应用程序中如何使用Unity的AOP功能(也就是Interception,以前好像是Policy Injection)。
在应用程序中使用Unity拦截(Interception)
事实上Unity Interception是Unity IoC的一个扩展,跟Interception相关的有两个单独的程序集:Microsoft.Practices.Unity.Interception和Microsoft.Practices.Unity.Interception.Configuration。因此,在使用Unity Interception的时候,需要另外引入这两个程序集。
首先,在Unity的配置信息中,加入sectionExtension节点:
<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration"/>
然后,对于需要执行AOP拦截的类型注册,添加拦截器的类型定义以及拦截行为的定义。比如,在Byteart Retail中,所有的AOP拦截操作都是在应用层服务上进行的,因此,Byteart Retail的Unity配置信息中会有类似如下的内容:
<register type="ByteartRetail.ServiceContracts.IUserService, ByteartRetail.ServiceContracts" mapTo="ByteartRetail.Application.Implementation.UserServiceImpl, ByteartRetail.Application"> <interceptor type="InterfaceInterceptor"/> <interceptionBehavior type="ByteartRetail.Infrastructure.InterceptionBehaviors.CachingBehavior, ByteartRetail.Infrastructure"/> <interceptionBehavior type="ByteartRetail.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, ByteartRetail.Infrastructure"/> </register>
在上面的配置信息中,interceptionBehavior节点指定了拦截行为定义,我们很容易看到,Byteart Retail使用了两种行为:缓存和异常处理。下面会对这两种拦截行为的具体实现进行介绍。
最后就是拦截行为的定义了。在Byteart Retail中,所有的拦截行为都定义在ByteartRetail.Infrastructure.InterceptionBehaviors命名空间中。基于Unity Interception的拦截行为通常需要实现Microsoft.Practices.Unity.InterceptionExtension.IInterceptionBehavior接口。该接口包含了一个只读属性WillExecute以及两个方法的定义:GetRequiredInterfaces和Invoke,其中最重要的就是Invoke方法。读者朋友可以参考Byteart Retail案例中CachingBehavior和ExceptionLoggingBehavior的实现来了解这些属性和方法。
拦截行为:异常处理
Byteart Retail中异常处理的拦截行为实现非常简单,就是判断被拦截的方法调用是否存在异常,如果存在,则通过Utils.Log方法将异常记录到日志中,而在底层,Utils.Log则是使用log4net来实现日志记录的。异常处理拦截行为的Invoke方法实现如下:
/// <summary> /// 通过实现此方法来拦截调用并执行所需的拦截行为。 /// </summary> /// <param name="input">调用拦截目标时的输入信息。</param> /// <param name="getNext">通过行为链来获取下一个拦截行为的委托。</param> /// <returns>从拦截目标获得的返回信息。</returns> public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { var methodReturn = getNext().Invoke(input, getNext); if (methodReturn.Exception != null) { Utils.Log(methodReturn.Exception); } return methodReturn; }
于是,当应用层服务调用出现异常时,我们就可以到文件系统中找到相关的异常信息,比如:
在实际项目中,异常处理的方式多种多样,在此仅通过异常记录,演示了基于AOP的异常处理方式。
拦截行为:缓存
缓存也是企业级应用程序中一种广泛使用的非常重要的技术。在Byteart Retail中,缓存不是通过某种特定的缓存机制来直接实现并使用的,而是以Unity Interception的一种拦截行为的方式实现的。缓存的引入,减少了系统的IO压力(比如减少了网络流量以及数据库磁盘读取次数),从而在一定程度上提高了系统性能。
上文已经大致提到,在Byteart Retail中,由于拦截行为都是基于应用层服务来实现的,因此,缓存也是基于应用层服务来设计的。具体含义是,比如,在引入缓存拦截行为之前,当客户端调用IProductService.GetProductByID时,应用层服务ProductService就会根据传入参数从数据库重建领域对象,然后转换成数据传输对象(DTO)返回;而在引入缓存拦截行为以后,ProductService则会首先查看缓存中是否存在所需的对象,如果存在,则直接返回;否则再到数据库中查找并返回,同时会将查找到的对象保存到缓存中,以备下次使用。当然在实现之前,最好也对需要缓存的对象进行一些分析,以便更好地实现缓存,比如在Byteart Retail案例中,对产品信息、产品分类等对象进行了缓存,因为这些信息都是查得多,改得少的,因此会对应用程序的响应度有很大的改善。
为了能够解耦应用程序与缓存机制的具体实现,Byteart Retail使用了代理(Proxy)模式、单件(Singleton)模式和服务定位器(Service Locator)模式设计了整个缓存机制。
首先,定义了一个“缓存供应商(Cache Provider)”的接口,该接口向应用程序提供了访问缓存机制的方法,包括:向缓存中添加对象、从缓存获取对象、判断对象是否存在于缓存等。代码如下:
/// <summary> /// 表示实现该接口的类型是能够为应用程序提供缓存机制的类型。 /// </summary> public interface ICacheProvider { #region Methods /// <summary> /// 向缓存中添加一个对象。 /// </summary> /// <param name="key">缓存的键值,该值通常是使用缓存机制的方法的名称。</param> /// <param name="valKey">缓存值的键值,该值通常是由使用缓存机制的方法的参数值所产生。</param> /// <param name="value">需要缓存的对象。</param> void Add(string key, string valKey, object value); /// <summary> /// 向缓存中更新一个对象。 /// </summary> /// <param name="key">缓存的键值,该值通常是使用缓存机制的方法的名称。</param> /// <param name="valKey">缓存值的键值,该值通常是由使用缓存机制的方法的参数值所产生。</param> /// <param name="value">需要缓存的对象。</param> void Put(string key, string valKey, object value); /// <summary> /// 从缓存中读取对象。 /// </summary> /// <param name="key">缓存的键值,该值通常是使用缓存机制的方法的名称。</param> /// <param name="valKey">缓存值的键值,该值通常是由使用缓存机制的方法的参数值所产生。</param> /// <returns>被缓存的对象。</returns> object Get(string key, string valKey); /// <summary> /// 从缓存中移除对象。 /// </summary> /// <param name="key">缓存的键值,该值通常是使用缓存机制的方法的名称。</param> void Remove(string key); /// <summary> /// 获取一个<see cref="Boolean"/>值,该值表示拥有指定键值的缓存是否存在。 /// </summary> /// <param name="key">指定的键值。</param> /// <returns>如果缓存存在,则返回true,否则返回false。</returns> bool Exists(string key); /// <summary> /// 获取一个<see cref="Boolean"/>值,该值表示拥有指定键值和缓存值键的缓存是否存在。 /// </summary> /// <param name="key">指定的键值。</param> /// <param name="valKey">缓存值键。</param> /// <returns>如果缓存存在,则返回true,否则返回false。</returns> bool Exists(string key, string valKey); #endregion }
接下来,缓存机制的具体实现(Byteart Retail提供了两种实现:基于Microsoft Patterns & Practices Enterprise Library Caching Application Block的缓存以及基于Windows Server Appfabric Caching的缓存,请参考ByteartRetail.Infrastructure.Caching命名空间)以及缓存管理器(Cache Manager)都实现了ICacheProvider接口,缓存管理器结合代理模式和服务定位器模式向应用程序提供缓存服务。相关类型之间的关系可以通过以下类图表示:
基于这样的设计,当应用程序需要使用缓存机制时,只需要调用CacheManager的相关方法即可。
接下来让我们再去了解一下在拦截行为中,缓存是如何使用的。缓存使用的大致过程是,系统首先会根据应用层服务接口中方法上的CachingAttribute特性设置,来判断当前被拦截方法的缓存使用方式,比如在IProductService.GetProductByID方法上,通过CachingAttribute设置缓存的使用方式是Get,这就意味着当客户端调用该方法时,拦截行为会根据传入参数,首先试图从缓存中获取对象,如果存在则直接返回;否则,拦截行为会进而调用被代理的应用层服务以获得对象,将其缓存起来然后返回。再比如在IProductService.DeleteProduct方法上,通过CachingAttribute设置缓存使用方式是Remove,这就意味着当客户端调用该方法时,拦截行为会将所有在指定方法中存入缓存的对象清除,然后再调用被代理的应用层服务,这就使得下次调用Get的方法时,缓存数据会被更新。比如在IProductService.DeleteProducts方法的定义上,CachingAttribute设置了所有会产生产品信息缓存的方法名称,而通过这些方法产生的缓存数据将会在DeleteProducts被调用的时候删除:
/// <summary> /// 删除商品信息。 /// </summary> /// <param name="productIDs">需要删除的商品信息的ID值。</param> [OperationContract] [FaultContract(typeof(FaultData))] [Caching(CachingMethod.Remove, "GetProductsForCategory", "GetProductsWithPagination", "GetFeaturedProducts", "GetProductsForCategoryWithPagination", "GetProducts", "GetProductByID")] void DeleteProducts(IDList productIDs);
其实,目前这样的实现方式还是会有些小问题的,比如一股脑地删掉所有的缓存对象必然不是最佳的方式,不过Byteart Retail主要是为了演示在AOP拦截中缓存的实现,所以也没有过细地考虑,读者朋友有兴趣的话可以继续优化一下。缓存拦截行为CachingBehavior的实现代码在此就不重复了,读者朋友请单击此处查看。最后让我们看看Byteart Retail在引入不同的缓存机制时,对系统性能的影响。以下是在不使用缓存机制、使用基于Appfabric的缓存以及使用Enterprise Library Caching Application Block的缓存三种情况下,调用IProductService.GetProductByID方法的响应时间对比(平均值),从结果上看,相同调用次数下,使用Enterprise Library Caching Application Block缓存的调用响应时间最短,而不使用缓存机制的调用响应时间最长。Appfabric Caching虽然没有Caching Application Block的响应时间短,但它能够提供更为灵活的、分布式的缓存管理解决方案,使得被缓存的数据能够被跨应用程序域的进程所使用。因此,在实际项目中具体采用何种方案,还需要视情况而定。
总结
本文详细介绍了Byteart Retail案例中基于AOP拦截的异常处理和缓存的实现方式,这些方式可以供读者朋友们参考使用,还可以根据这种思想实现更多的Cross-Cutting的功能,例如安全验证等。最后再回顾一下使用AOP的初衷:我们确实没有将任何技术内容带入领域模型中。