一、引言
在前面一專題介紹到,要讓緩存生效還需要實現對AOP(面向切面編程)的支持。所以本專題將介紹了網上書店案例中AOP的實現。關於AOP的概念,大家可以參考文章:http://www.cnblogs.com/jin-yuan/p/3811077.html。這里我簡單介紹下AOP:AOP可以理解為對方法進行截獲,這樣就可以在方法調用前或調用后插入需要的邏輯。例如可以在方法調用前,加入緩存查找邏輯等。這里緩存查找邏輯就在方法調用前被執行。通過對AOP的支持,每個方法就可以分為3部分了,方法調用前邏輯->具體需要調用的方法->方法調用后的邏輯。也就是在方法調用的時候“切了一刀”。
二、網上書店AOP的實現
你可以從零開始去實現AOP,但是目前已經存在很多AOP框架了,所以在本案例中將直接通過Unity的AOP框架(Unity.Interception)來實現網上書店對AOP的支持。通常AOP的實現放在基礎設施層進行實現,因為可能其他所有層都需要加入對AOP的支持。本案例中將對兩個方面的AOP進行實現,一個是方法調用前緩存的記錄或查找,另一個是方法調用后異常信息的記錄。在實現具體代碼之前,我們需要在基礎設施層通過Nuget來引入Unity.Interception包。添加成功之后,我們需要定義兩個類分別去實現AOP框架中IInterceptionBehavior接口。由於本案例中需要對緩存和異常日志功能進行AOP實現,自然就需要定義CachingBehavior和ExceptionLoggingBehavior兩個類去實現IInterceptionBehavior接口。首先讓我們看看CachingBehavior類的實現,具體實現代碼如下所示:
// 緩存AOP的實現 public class CachingBehavior : IInterceptionBehavior { private readonly ICacheProvider _cacheProvider; public CachingBehavior() { _cacheProvider = ServiceLocator.Instance.GetService<ICacheProvider>(); } // 生成緩存值的鍵值 private string GetValueKey(CacheAttribute cachingAttribute, IMethodInvocation input) { switch (cachingAttribute.Method) { // 如果是Remove,則不存在特定值鍵名,所有的以該方法名稱相關的緩存都需要清除 case CachingMethod.Remove: return null; // 如果是Get或者Update,則需要產生一個針對特定參數值的鍵名 case CachingMethod.Get: case CachingMethod.Update: if (input.Arguments != null && input.Arguments.Count > 0) { var sb = new StringBuilder(); for (var i = 0; i < input.Arguments.Count; i++) { sb.Append(input.Arguments[i]); if (i != input.Arguments.Count - 1) sb.Append("_"); } return sb.ToString(); } else return "NULL"; default: throw new InvalidOperationException("無效的緩存方式。"); } } #region IInterceptionBehavior Members public IEnumerable<Type> GetRequiredInterfaces() { return Type.EmptyTypes; } public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { // 獲得被攔截的方法 var method = input.MethodBase; var key = method.Name; // 獲得攔截的方法名 // 如果攔截的方法定義了Cache屬性,說明需要對該方法的結果需要進行緩存 if (!method.IsDefined(typeof (CacheAttribute), false)) return getNext().Invoke(input, getNext); var cachingAttribute = (CacheAttribute)method.GetCustomAttributes(typeof (CacheAttribute), false)[0]; var valueKey = GetValueKey(cachingAttribute, input); switch (cachingAttribute.Method) { case CachingMethod.Get: try { // 如果緩存中存在該鍵值的緩存,則直接返回緩存中的結果退出 if (_cacheProvider.Exists(key, valueKey)) { var value = _cacheProvider.Get(key, valueKey); var arguments = new object[input.Arguments.Count]; input.Arguments.CopyTo(arguments, 0); return new VirtualMethodReturn(input, value, arguments); } else // 否則先調用方法,再把返回結果進行緩存 { var methodReturn = getNext().Invoke(input, getNext); _cacheProvider.Add(key, valueKey, methodReturn.ReturnValue); return methodReturn; } } catch (Exception ex) { return new VirtualMethodReturn(input, ex); } case CachingMethod.Update: try { var methodReturn = getNext().Invoke(input, getNext); if (_cacheProvider.Exists(key)) { if (cachingAttribute.IsForce) { _cacheProvider.Remove(key); _cacheProvider.Add(key, valueKey, methodReturn.ReturnValue); } else _cacheProvider.Update(key, valueKey, methodReturn); } else _cacheProvider.Add(key, valueKey, methodReturn.ReturnValue); return methodReturn; } catch (Exception ex) { return new VirtualMethodReturn(input, ex); } case CachingMethod.Remove: try { var removeKeys = cachingAttribute.CorrespondingMethodNames; foreach (var removeKey in removeKeys) { if (_cacheProvider.Exists(removeKey)) _cacheProvider.Remove(removeKey); } // 執行具體截獲的方法 var methodReturn = getNext().Invoke(input, getNext); return methodReturn; } catch (Exception ex) { return new VirtualMethodReturn(input, ex); } default: break; } return getNext().Invoke(input, getNext); } public bool WillExecute { get { return true; } } #endregion }
從上面代碼可以看出,通過Unity.Interception框架來實現AOP變得非常簡單了,我們只需要實現IInterceptionBehavior接口中的Invoke方法和WillExecute屬性即可。並且從上面代碼可以看出,AOP的支持最核心代碼實現在於Invoke方法的實現。既然我們需要在方法調用前查找緩存,如果緩存不存在再調用方法從數據庫中進行查找,如果存在則直接從緩存中進行讀取數據即可。自然需要在 getNext().Invoke(input, getNext)代碼執行前進緩存進行查找,然而上面CachingBehavior類正式這樣實現的。
介紹完緩存功能AOP的實現之后,下面具體看看異常日志的AOP實現。具體實現代碼如下所示:
// 用於異常日志記錄的攔截行為 public class ExceptionLoggingBehavior :IInterceptionBehavior { /// <summary> /// 需要攔截的對象類型的接口 /// </summary> /// <returns></returns> public IEnumerable<Type> GetRequiredInterfaces() { return Type.EmptyTypes; } /// <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; } // 表示當攔截行為被調用時,是否需要執行某些操作 public bool WillExecute { get { return true; } } }
異常日志功能的AOP實現與緩存功能的AOP實現類似,只是一個需要在方法執行前注入,而一個是在方法執行后進行注入罷了,其實現原理都是在截獲的方法前后進行。方法截獲功能AOP框架已經幫我們實現了。
到此,我們網上書店AOP的實現就已經完成了,但要正式生效還需要通過配置文件把AOP的實現注入到需要截獲的方法當中去,這樣執行這些方法才會執行注入的行為。對應的配置文件如下標紅部分所示:
<!--Unity的配置信息--> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" /> <container> <extension type="Interception" /> <!--Cache Provider--> <register type="OnlineStore.Infrastructure.Caching.ICacheProvider, OnlineStore.Infrastructure" mapTo="OnlineStore.Infrastructure.Caching.EntLibCacheProvider, OnlineStore.Infrastructure" /> <!--倉儲接口的注冊--> <register type="OnlineStore.Domain.Repositories.IRepositoryContext, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.EntityFrameworkRepositoryContext, OnlineStore.Repositories"> <lifetime type="singleton" /> </register> <register type="OnlineStore.Domain.Repositories.IProductRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ProductRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.ICategoryRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.CategoryRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IProductCategorizationRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ProductCategorizationRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IUserRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IShoppingCartRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ShoppingCartRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IShoppingCartItemRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ShoppingCartItemRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IOrderRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.OrderRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IUserRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IUserRoleRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRoleRepository, OnlineStore.Repositories" /> <register type="OnlineStore.Domain.Repositories.IRoleRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.RoleRepository, OnlineStore.Repositories" /> <!--Domain Services--> <register type="OnlineStore.Domain.Services.IDomainService, OnlineStore.Domain" mapTo="OnlineStore.Domain.Services.DomainService, OnlineStore.Domain" /> <!--應用服務的注冊--> <register type="OnlineStore.ServiceContracts.IProductService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.ProductServiceImp, OnlineStore.Application"> <!--注入AOP功能的實現--> <interceptor type="InterfaceInterceptor" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" /> </register> <register type="OnlineStore.ServiceContracts.IOrderService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.OrderServiceImp, OnlineStore.Application"> <!--注入AOP功能的實現--> <interceptor type="InterfaceInterceptor" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" /> </register> <register type="OnlineStore.ServiceContracts.IUserService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.UserServiceImp, OnlineStore.Application"> <!--注入AOP功能的實現--> <interceptor type="InterfaceInterceptor" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" /> <interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" /> </register> <!--Domain Event Handlers--> <register type="OnlineStore.Domain.Events.IDomainEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Domain" mapTo="OnlineStore.Domain.Events.EventHandlers.OrderDispatchedEventHandler, OnlineStore.Domain" name="OrderDispatchedEventHandler" /> <register type="OnlineStore.Domain.Events.IDomainEventHandler`1[[OnlineStore.Domain.Events.OrderConfirmedEvent, OnlineStore.Domain]], OnlineStore.Domain" mapTo="OnlineStore.Domain.Events.EventHandlers.OrderConfirmedEventHandler, OnlineStore.Domain" name="OrderConfirmedEventHandler" /> <!--Event Handlers--> <register name="orderSendEmailHandler" type="OnlineStore.Events.IEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Events" mapTo="OnlineStore.Events.Handlers.SendEmailHandler, OnlineStore.Events.Handlers" /> <!--Event Aggregator--> <register type="OnlineStore.Events.IEventAggregator, OnlineStore.Events" mapTo="OnlineStore.Events.EventAggregator, OnlineStore.Events"> <constructor> <param name="handlers"> <array> <dependency name="orderSendEmailHandler" type="OnlineStore.Events.IEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Events" /> </array> </param> </constructor> </register> <!--Event Bus--> <!--<register type="OnlineStore.Events.Bus.IEventBus, OnlineStore.Events" mapTo="OnlineStore.Events.Bus.EventBus, OnlineStore.Events"> <lifetime type="singleton" /> </register>--> <!--注入MsmqEventBus--> <register type="OnlineStore.Events.Bus.IEventBus, OnlineStore.Events" mapTo="OnlineStore.Events.Bus.MsmqEventBus, OnlineStore.Events"> <lifetime type="singleton" /> <constructor> <param name="path" value=".\Private$\OnlineStoreQueue" /> </constructor> </register> </container> </unity> <!--END: Unity-->
到此,網上書店案例中AOP的實現就完成了。通過上面的配置可以看出,客戶端在調用應用服務方法前后會調用我們注入的行為,即緩存行為和異常日志行為。通過對AOP功能的支持,就不需要為每個需要進行緩存或需要異常日志行為的方法來重復寫這些相同的邏輯了。從而避免了重復代碼的重復實現,提高了代碼的重用性和降低了模塊之間的依賴性。
三、網上書店案例中站點地圖的實現
在大部分網站中都實現了站點地圖的功能,在Asp.net中,我們可以通過SiteMap模塊來實現站點地圖的功能,在Asp.net MVC也可以通過MvcSiteMapProvider第三方開源框架來實現站點地圖。所以針對網上書店案例,站點地圖的支持也是必不可少的。下面讓我們具體看看站點地圖在本案例中是如何去實現的呢?
在看實現代碼之前,讓我們先來理清下實現思路。
本案例中站點地圖的實現,並沒有借助MvcSiteMapProvider第三方框架來實現。其實現原理首先獲得用戶的路由請求,然后根據用戶請求根據站點地圖的配置獲得對應的配置節點,接着根據站點地圖的節點信息生成類似">首頁>"這樣帶標簽的字符串;如果獲得的節點是配置文件中某個父節點的子節點,此時會通過遞歸的方式找到其父節點,然后遞歸地生成對應帶標簽的字符串,從而完成站點地圖的功能。分析完實現思路之后,下面讓我們再對照下具體的實現代碼來加深理解。具體的實現代碼如下所示:
public class MvcSiteMap { private static readonly MvcSiteMap _instance = new MvcSiteMap(); private static readonly XDocument Doc = XDocument.Load(HttpContext.Current.Server.MapPath(@"~/SiteMap.xml")); private UrlHelper _url = null; private string _currentUrl; public static MvcSiteMap Instance { get { return _instance;} } private MvcSiteMap() { } public MvcHtmlString Navigator() { // 獲得當前請求的路由信息 _url = new UrlHelper(HttpContext.Current.Request.RequestContext); var routeUrl = _url.RouteUrl(HttpContext.Current.Request.RequestContext.RouteData.Values); if (routeUrl != null) _currentUrl = routeUrl.ToLower(); // 從配置的站點Xml文件中找到當前請求的Url相同的節點 var c = FindNode(Doc.Root); var temp = GetPath(c); return MvcHtmlString.Create(BuildPathString(temp)); } // 從SitMap配置文件中找到當前請求匹配的節點 private XElement FindNode(XElement node) { // 如果xml節點對應的url是否與當前請求的節點相同,如果相同則直接返回xml對應的節點 // 如果不同開始遞歸子節點 return IsUrlEqual(node) == true ? node : RecursiveNode(node); } // 判斷xml節點對應的url是否與當前請求的url一樣 private bool IsUrlEqual(XElement c) { var a = GetNodeUrl(c).ToLower(); return a == _currentUrl; } // 遞歸Xml節點 private XElement RecursiveNode(XElement node) { foreach (var c in node.Elements()) { if (IsUrlEqual(c) == true) { return c; } else { var x = RecursiveNode(c); if (x != null) { return x; } } } return null; } // 獲得xml節點對應的請求url private string GetNodeUrl(XElement c) { return _url.Action(c.Attribute("action").Value, c.Attribute("controller").Value, new {area = c.Attribute("area").Value}); } // 根據對應請求url對應的Xml節點獲得其在Xml中的路徑,即獲得其父節點有什么 // SiteMap.xml 中節點的父子節點一定要配置對 private Stack<XElement> GetPath(XElement c) { var temp = new Stack<XElement>(); while (c != null) { temp.Push(c); c = c.Parent; } return temp; } // 根據節點的路徑來拼接帶標簽的字符串 private string BuildPathString(Stack<XElement> m) { var sb = new StringBuilder(); var tc = new TagBuilder("span"); tc.SetInnerText(">"); var sp = tc.ToString(); var count = m.Count; for (var x = 1; x <= count; x++) { var c = m.Pop(); TagBuilder tb; if (x == count) { tb = new TagBuilder("span"); } else { tb = new TagBuilder("a"); tb.MergeAttribute("href", GetNodeUrl(c)); } tb.SetInnerText(c.Attribute("title").Value); sb.Append(tb); sb.Append(sp); } return sb.ToString(); } }
對應的站點地圖配置信息如下所示:
<?xml version="1.0" encoding="utf-8" ?> <node title="首頁" area="" action="Index" controller="Home"> <node title="我的" area="UserCenter" action="Manage" controller="Account"> <node title="訂單" area="" action="Orders" controller="Home" /> <node title="賬戶" area="" action="Manage" controller="Account" /> <node title="購物車" area="" action="ShoppingCart" controller="Home" /> </node> <node title="關於" area="AboutCenter" action="About" controller="Home" > <node title="Online Store 項目" area="" action="About" controller="Home" /> <node title="聯系方式" area="" action="Contact" controller="Home" /> </node> </node>
實現完成之后,下面讓我們具體看看本案例中站點地圖的實現效果看看,具體運行效果如下圖所示:
四、小結
到這里,本專題的內容就結束了。本專題主要借助Unity.Interception框架在網上書店中引入了AOP功能,並且最后簡單介紹了站點地圖的實現。在下一專題將對CQRS模式做一個全面的介紹。
本案例所有源碼:https://github.com/lizhi5753186/OnlineStore_Second/