.Net中的AOP系列之《方法执行前后——边界切面》
返回《.Net中的AOP》系列學(xué)習(xí)總目錄
本篇目錄
- 邊界切面
- PostSharp方法邊界
- 方法邊界 VS 方法攔截
- ASP.NET HttpModule邊界
- 真實(shí)案例——檢查是否為移動(dòng)端用戶(hù)
- 真實(shí)案例——緩存
- 小結(jié)
本系列的源碼本人已托管于Coding上:點(diǎn)擊查看。
本系列的實(shí)驗(yàn)環(huán)境:VS 2013 Update 5(建議最好使用集成了Nuget的VS版本,VS Express版也夠用),安裝PostSharp。
這篇博客覆蓋的內(nèi)容包括:
- 什么是方法邊界
- 使用PostSharp的邊界方法
- 編寫(xiě)ASP.NET HttpModule來(lái)檢測(cè)用戶(hù)是否是移動(dòng)端用戶(hù)
- 方法攔截和方法邊界的不同之處
- 使用PostSharp編寫(xiě)緩存切面
上一篇我們講了方法切面中最通用的類(lèi)型《方法攔截》,這篇我們講一下可能用到的另外一種切面:邊界切面,它里面的代碼會(huì)在方法的邊界運(yùn)行。首先會(huì)使用PostSharp在方法層面上演示一個(gè)邊界切面,然后也會(huì)使用ASP.NET HttpModule演示一個(gè)頁(yè)面層面的邊界切面。
這篇博客的目的是通過(guò)多個(gè)例子演示來(lái)說(shuō)明什么事邊界切面以及邊界切面一般是如何工作的,而不是帶領(lǐng)大家詳細(xì)地編寫(xiě)這些例子。
邊界切面
通常意義上的邊界指的是兩個(gè)實(shí)體間的任意分割線,比如兩個(gè)國(guó)家之間的地理上的分界線,我國(guó)各個(gè)省份之間的分界線,當(dāng)你去臨省旅游時(shí),你必須首先穿過(guò)你所在省和鄰省的分界線,旅行結(jié)束返回時(shí),必須再次穿過(guò)省份分界線。
和現(xiàn)實(shí)生活一樣,編碼時(shí)也會(huì)有很多分界線。就拿最簡(jiǎn)單的控制臺(tái)程序來(lái)說(shuō),當(dāng)啟動(dòng)一個(gè)Main方法時(shí),然后Main方法又調(diào)用了另一個(gè)方法,當(dāng)程序進(jìn)入被調(diào)用方法體時(shí)也要穿過(guò)一個(gè)分界線,當(dāng)被調(diào)方法執(zhí)行完成之后,程序流就會(huì)返回到Main方法,這就是我們平時(shí)沒(méi)怎么意識(shí)到的邊界。
使用了AOP,我們就可以把代碼放到那些邊界上,這些邊界代表了一個(gè)地方或一個(gè)條件,對(duì)于放置一些可復(fù)用的代碼很有用。
PostSharp方法邊界
創(chuàng)建一個(gè)控制臺(tái)項(xiàng)目,名為“BasketballStatsPostSharp”,解決方案名稱(chēng)為"BoundaryAspectsPractices",通過(guò)Nuget安裝PostSharp。這個(gè)項(xiàng)目的需求很簡(jiǎn)單,創(chuàng)建一個(gè)服務(wù)類(lèi),然后根據(jù)球員的名字獲得該球員的球衣號(hào)碼,這里為了演示,直接將結(jié)果打印到控制臺(tái)。
public class BasketballStatsService {/// <summary>/// 根據(jù)球員的名字返回球員的球衣號(hào)碼/// </summary>/// <param name="playerName"></param>/// <returns></returns>public string GetPlayerNumber(string playerName){if (playerName.Equals("Michael Jordan")){return 23.ToString();}if (playerName.Equals("Kobe Bryant")){return 24.ToString();}return 0.ToString();} }class Program {static void Main(string[] args){//這個(gè)花括號(hào)是程序沒(méi)有執(zhí)行和開(kāi)始執(zhí)行的分界線var service=new BasketballStatsService();var playName = "Michael Jordan";var no1 = service.GetPlayerNumber(playName);//這里是Main方法和GetPlayerNumber方法的分界線Console.WriteLine("{0}的球衣號(hào)碼是{1}",playName,no1);Console.Read();}//這個(gè)花括號(hào)是程序結(jié)束前和程序結(jié)束后的分界線 }這只是個(gè)普通的程序,沒(méi)什么可言之處,大家很容易看出運(yùn)行結(jié)果,這里就不演示了。
下面我們創(chuàng)建一個(gè)邊界切面MyBoundaryAspect,它繼承自PostSharp中的OnMethodBoundaryAspect,注意使用PostSharp時(shí)記得使用Serializable特性。
使用的話,很簡(jiǎn)單,只需要在服務(wù)類(lèi)的方法上加上特性即可,然后運(yùn)行如下:
這個(gè)例子和第一篇介紹中的"Hello World"例子差不多,沒(méi)什么好玩的,別著急,在本文后面會(huì)有一個(gè)使用邊界方法處理緩存的例子。
方法邊界 VS 方法攔截
目前,方法邊界切面和方法攔截切面我們都看過(guò)了,那么接下來(lái)對(duì)比一下這兩者有什么區(qū)別。區(qū)別肯定是存在的,但這些區(qū)別是很微妙的,專(zhuān)一的開(kāi)發(fā)者可能只使用其中一種切面。這節(jié)從下面兩個(gè)方面討論一下這些區(qū)別:
下圖是PostSharp中MethodInterceptionAspect和OnMethodBoundary切面的基本結(jié)構(gòu)對(duì)比:
概念上講,可以將一個(gè)邊界切面轉(zhuǎn)成攔截切面,反之亦然,只需要將左邊的代碼改為右邊格式的代碼就好了,但是,如果真那么簡(jiǎn)單,那么這兩者之間的區(qū)別是什么呢?很明顯,答案肯定不是想象的那么簡(jiǎn)單。
切面方法間的共享狀態(tài)
首先看一下共享狀態(tài)。攔截切面只有一個(gè)方法OnInvoke,因此共享狀態(tài)不是關(guān)心的問(wèn)題——在方法開(kāi)始時(shí)可以使用的任何變量可以繼續(xù)在方法的其他地方使用。但是對(duì)于邊界方法來(lái)說(shuō)就不那么簡(jiǎn)單了,在OnEntry方法中聲明的變量在OnSuccess方法中是不可用的,因?yàn)樗鼈兪欠蛛x的方法。
但使用PostSharp,對(duì)于邊界方法的共享狀態(tài)可以變通一下。首先,可以使用類(lèi)本身的字段:
[Serializable]public class MyBoundaryAspect:OnMethodBoundaryAspect{private string _sharedState;//使用一個(gè)全局變量共享方法之間的信息public override void OnEntry(MethodExecutionArgs args){_sharedState = "123";//邊界方法運(yùn)行之前,設(shè)置一個(gè)值Console.WriteLine("方法{0}執(zhí)行前",args.Method.Name);}public override void OnSuccess(MethodExecutionArgs args){Console.WriteLine("方法{0}執(zhí)行后,_sharedState={1}", args.Method.Name,_sharedState);//邊界方法運(yùn)行之后該值不變}}然而,這種方法有個(gè)缺點(diǎn)。在PostSharp中,切面類(lèi)中的每個(gè)邊界方法都使用切面類(lèi)的相同實(shí)例。這種切面叫做靜態(tài)范圍切面,這意味著,即使你創(chuàng)建了多個(gè)類(lèi)的實(shí)例,PostSharp的切面標(biāo)記的方法只會(huì)創(chuàng)建一個(gè)切面實(shí)例與那個(gè)類(lèi)對(duì)應(yīng)。如果切面實(shí)現(xiàn)了IInstanceScopedAspect接口,那么這個(gè)切面就是一個(gè)實(shí)例范圍切面。默認(rèn)行為會(huì)在編織之后,在代碼中添加少量負(fù)擔(dān),但是引入的那點(diǎn)復(fù)雜度可能不是很明顯。
要演示這個(gè)問(wèn)題,修改一下切面類(lèi)和Main方法,服務(wù)類(lèi)方法不變,代碼修改如下:
[Serializable]public class MyBoundaryAspect:OnMethodBoundaryAspect{private readonly Guid _sharedState;//使用一個(gè)全局變量共享方法之間的信息public MyBoundaryAspect(){_sharedState = Guid.NewGuid();}public override void OnEntry(MethodExecutionArgs args){//_sharedState = "123";//邊界方法運(yùn)行之前,設(shè)置一個(gè)值Console.WriteLine("方法{0}執(zhí)行前",args.Method.Name);}public override void OnSuccess(MethodExecutionArgs args){Console.WriteLine("方法{0}執(zhí)行后,_sharedState={1}", args.Method.Name,_sharedState);//邊界方法運(yùn)行之后該值不變}}#region 攔截切面VS邊界切面var s1=new BasketballStatsService();var s2=new BasketballStatsService();s1.GetPlayerNumber("Kobe Bryant");s2.GetPlayerNumber("Kobe Bryant");#endregionConsole.Read();運(yùn)行效果如下:
從結(jié)果可以看到,產(chǎn)生的GUID的值是一樣的,也就是說(shuō),切面實(shí)例(每次實(shí)例化時(shí)都會(huì)產(chǎn)生)只產(chǎn)生了一個(gè),也就是說(shuō)多個(gè)服務(wù)類(lèi)的方法共享了相同的MyBoundaryAspect切面對(duì)象。如果又調(diào)用了服務(wù)類(lèi)的另外一個(gè)方法,那么生成的GUID的值就不同了。
GUID
GUID是Globally Unique Identifier(全局唯一標(biāo)識(shí)符)的簡(jiǎn)寫(xiě)。GUID是用于唯一標(biāo)識(shí)的128bit的值,通常表現(xiàn)為16進(jìn)制的8-4-4-4-12形式。Guid.NewGuid()會(huì)生產(chǎn)一個(gè)唯一的Guid(不是從數(shù)學(xué)角度,而是從實(shí)際和統(tǒng)計(jì)角度),因此很適合演示產(chǎn)生的實(shí)例是不是同一個(gè)實(shí)例。
總之,切面的全局字段不是切面方法間溝通的安全方式,因?yàn)樗皇蔷€程安全的。其他方法可以對(duì)這些全局字段更改,因此,PostSharp提供了一個(gè)叫做args.MethodExecutionTag的API來(lái)協(xié)助共享狀態(tài)。它是會(huì)傳入每個(gè)邊界方法的args對(duì)象的屬性,該對(duì)象對(duì)于方法調(diào)用時(shí)的每次特定時(shí)間都是唯一的。
現(xiàn)在,將Guid.NewGuid()移到構(gòu)造函數(shù)的外面的OnEntry方法中,然后在OnSuccess方法中使用args.MethodExecutionTag方式輸出。代碼如下:
[Serializable] public class MyBoundaryAspect:OnMethodBoundaryAspect {private readonly Guid _sharedState;//使用一個(gè)全局變量共享方法之間的信息public MyBoundaryAspect(){// _sharedState = Guid.NewGuid();}public override void OnEntry(MethodExecutionArgs args){//_sharedState = "123";//邊界方法運(yùn)行之前,設(shè)置一個(gè)值args.MethodExecutionTag = Guid.NewGuid();Console.WriteLine("方法{0}執(zhí)行前,該方法生成的Guid={1}",args.Method.Name,args.MethodExecutionTag);}public override void OnSuccess(MethodExecutionArgs args){//Console.WriteLine("方法{0}執(zhí)行后,_sharedState={1}", args.Method.Name,_sharedState);//邊界方法運(yùn)行之后該值不變Console.WriteLine("方法{0}執(zhí)行后,該方法生成的Guid={1}", args.Method.Name, args.MethodExecutionTag);} }運(yùn)行結(jié)果如下:
從上面的運(yùn)行結(jié)果看以看出,同一個(gè)邊界切面中的不同邊界方法共享了相同的數(shù)據(jù)GUID,但是不同的服務(wù)類(lèi)實(shí)例調(diào)用使用了同一個(gè)切面的方法,GUID是不同的。
MethodExecutionTag是一個(gè)對(duì)象類(lèi)型,適合存儲(chǔ)一些像GUID等簡(jiǎn)單的類(lèi)型,如果需要存儲(chǔ)更復(fù)雜的共享數(shù)據(jù),必須在使用時(shí)強(qiáng)制轉(zhuǎn)換MethodExecutionTag的類(lèi)型。如果要存儲(chǔ)一個(gè)包含了多個(gè)對(duì)象的共享數(shù)據(jù),必須創(chuàng)建一個(gè)自定義類(lèi)存儲(chǔ)到MethodExecutionTag屬性中。
記住,方法攔截切面中不存在這些問(wèn)題,因?yàn)镺nInvoke方法是方法攔截切面中唯一的方法,可以在該方法中使用所有的共享數(shù)據(jù)。上一篇例子中的數(shù)據(jù)事務(wù)就是一個(gè)使用了很多共享數(shù)據(jù)的例子,比如重試次數(shù)的數(shù)量,事務(wù)是否成功執(zhí)行的標(biāo)識(shí)succeeded都是共享數(shù)據(jù)。
那么如何選擇何時(shí)使用攔截切面還是邊界切面呢?方法是:如果你要編寫(xiě)的切面使用了復(fù)雜的共享數(shù)據(jù),或者使用了很多共享數(shù)據(jù),那么最好使用方法攔截切面。
代碼清晰度/意圖
方法攔截切面在數(shù)據(jù)共享方法有明顯的優(yōu)勢(shì),但沒(méi)有共享數(shù)據(jù)或者共享數(shù)據(jù)很少呢?或者需要在某個(gè)單獨(dú)的邊界執(zhí)行一些代碼呢?這些場(chǎng)合,方法邊界切面更勝一籌。
下面寫(xiě)一個(gè)切面,該切面運(yùn)行在方法完成時(shí)的邊界(無(wú)論方法是否成功)。在PostSharp中需要編寫(xiě)這個(gè)邊界切面,
需要重寫(xiě)OnExit方法,它不同于OnSuccess方法,后者只有當(dāng)方法沒(méi)有拋出異常執(zhí)行完畢時(shí)才會(huì)執(zhí)行,而前者當(dāng)方法執(zhí)行完成時(shí)都會(huì)運(yùn)行,不管有沒(méi)有拋異常都會(huì)執(zhí)行。
如果要在攔截切面中寫(xiě)的話,就需要這么寫(xiě):
public class MyIntercepor : MethodInterceptionAspect {public override void OnInvoke(MethodInterceptionArgs args){try{args.Proceed();//在邊界切面中,這行代碼是隱式執(zhí)行的}finally //C#中的finally指的是,無(wú)論try中發(fā)生了什么,代碼塊都會(huì)執(zhí)行{Console.WriteLine("方法{0}執(zhí)行完成!", args.Method.Name);}} }上面這個(gè)例子很簡(jiǎn)單,但是現(xiàn)實(shí)中的項(xiàng)目不可能這么簡(jiǎn)單,可能try和finally代碼塊中的代碼都很多,那么此時(shí)使用攔截切面維護(hù)就顯得更加費(fèi)力,因?yàn)榈谝谎劭吹么a更多,而且代碼一多,可能發(fā)生的問(wèn)題更多。而邊界切面隱藏了try/catch/finally和Proceed()的細(xì)節(jié),我們不需要讀寫(xiě)那些代碼。
最后要說(shuō)的是,雖然你可能偏愛(ài)方法攔截,但不要忽略了邊界切面,因?yàn)樗梢愿纳拼a的清晰度和簡(jiǎn)潔度。
性能和內(nèi)存考慮
方法邊界切面和方法攔截切面其他的重要區(qū)別是性能和內(nèi)存方面,這些方法的考慮取決于使用的工具的不同而不同。
在PostSharp中,當(dāng)使用MethodInterceptionAspect時(shí),所有的參數(shù)每次都會(huì)從棧中復(fù)制到堆中(通過(guò)裝箱boxing),當(dāng)使用OnMethodBoundaryAspect時(shí),PostSharp會(huì)檢測(cè)沒(méi)有使用的參數(shù),不會(huì)把這些參數(shù)裝箱,從而優(yōu)化了代碼。因此,如果編寫(xiě)的切面沒(méi)有使用方法參數(shù),那么使用OnMethodBoundaryAspect會(huì)使用更少的內(nèi)存,如果在多個(gè)地方都使用這個(gè)切面,那么這樣的做法可能是重要的(注意:該優(yōu)化功能沒(méi)有包含在PostSharp的免費(fèi)版中)。
方法邊界不是使用AOP時(shí)唯一有用的邊界類(lèi)型,下面我們會(huì)看一個(gè)ASP.NET HttpModule的例子,這個(gè)例子對(duì)于把邊界放到web頁(yè)面上非常有用。
ASP.NET HttpModule邊界
這里為了方便演示,創(chuàng)建一個(gè)Asp.Net Web Form項(xiàng)目WebFormHttpModule,新建一個(gè)頁(yè)面Demo.aspx,代碼如下:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Demo.aspx.cs" Inherits="WebFormHttpModule.Demo" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"><title></title> </head> <body><form id="form1" runat="server"><div><h1>這是一個(gè)Demo頁(yè)面!</h1></div></form> </body> </html>在瀏覽器中瀏覽該文件時(shí),頁(yè)面上會(huì)顯示這是一個(gè)Demo頁(yè)面!這句話。對(duì)每個(gè)ASP.NET 頁(yè)面的請(qǐng)求都會(huì)有一個(gè)很復(fù)雜的生命周期,但值得注意的是該生命周期中的一部分使用了HttpModule,它允許我們將代碼放到ASP.NET頁(yè)面的邊界。要?jiǎng)?chuàng)建一個(gè)HttpModule,需要?jiǎng)?chuàng)建一個(gè)實(shí)現(xiàn)了IHttpModule接口的類(lèi):
public class MyHttpModule:IHttpModule {/// <summary>/// 釋放所有的資源和數(shù)據(jù)庫(kù)連接/// </summary>public void Dispose(){throw new NotImplementedException();}/// <summary>/// 當(dāng)HttpApplication的實(shí)例創(chuàng)建時(shí)運(yùn)行/// </summary>/// <param name="context"></param>public void Init(HttpApplication context){throw new NotImplementedException();} }每個(gè)模塊都必須在ASP.NET的Web.config文件中配置后才可以運(yùn)行。Web.config的配置可能會(huì)根據(jù)你使用的web服務(wù)器( IIS6 , IIS7 +, Cassini, IIS Express等等)不同而不同。要想覆蓋以上服務(wù)器的所有配置,可以像下面那樣配置:
<!--II6和ASP.NET開(kāi)發(fā)服務(wù)器會(huì)在這里尋找--> <system.web><compilation debug="true" targetFramework="4.5" /><httpModules><!--每個(gè)模塊需要唯一的名字和類(lèi)型(全命名空間+類(lèi)名)--><add name="MyHttpModule" type="WebFormHttpModule.MyHttpModule"/></httpModules> </system.web><system.webServer><validation validateIntegratedModeConfiguration="false"/><modules><add name="MyHttpModule" type="WebFormHttpModule.MyHttpModule"/></modules> </system.webServer>ASP.NET使用了多個(gè)工作進(jìn)程處理即將到來(lái)的請(qǐng)求,每個(gè)工作進(jìn)程都會(huì)創(chuàng)建一個(gè)HttpApplication的實(shí)例,每個(gè)HttpApplication實(shí)例都會(huì)創(chuàng)建一個(gè)HttpModule,然后運(yùn)行Init方法,現(xiàn)在自定義的Init方法什么都還沒(méi)寫(xiě),下面會(huì)使用事件句柄設(shè)置一些邊界:
public class MyHttpModule:IHttpModule {/// <summary>/// 釋放所有的資源和數(shù)據(jù)庫(kù)連接/// </summary>public void Dispose(){throw new NotImplementedException();}/// <summary>/// 當(dāng)HttpApplication的實(shí)例創(chuàng)建時(shí)運(yùn)行/// </summary>/// <param name="context"></param>public void Init(HttpApplication context){context.BeginRequest += context_BeginRequest;context.EndRequest += context_EndRequest;}/// <summary>/// 在所有的其他頁(yè)面生命周期事件結(jié)束之后運(yùn)行/// </summary>/// <param name="sender"></param>/// <param name="e"></param>void context_EndRequest(object sender, EventArgs e){var app = sender as HttpApplication;app.Response.Write("頁(yè)面所有的生命周期事件結(jié)束之后");}/// <summary>/// 頁(yè)面處理請(qǐng)求之前運(yùn)行/// </summary>/// <param name="sender"></param>/// <param name="e"></param>void context_BeginRequest(object sender, EventArgs e){var app = sender as HttpApplication;app.Response.Write("頁(yè)面請(qǐng)求處理之前");} }雖然語(yǔ)法很不同,但是這種感覺(jué)很像之前的方法邊界切面。瀏覽一下頁(yè)面,效果如下:
因?yàn)樵谶@些邊界方法中有一個(gè)HttpApplication對(duì)象,因此可以有很大的靈活性和潛能完成很多事情。當(dāng)檢查HttpApplication的屬性和事件時(shí),可以看到做許多事情而不僅是輸出文本。下一節(jié)我們會(huì)使用HttpModule演示一個(gè)真實(shí)的案例:檢測(cè)用戶(hù)是否是移動(dòng)端用戶(hù)。
真實(shí)案例——檢查是否為移動(dòng)端用戶(hù)
下面再創(chuàng)建一個(gè)ASP.NET WebForm 項(xiàng)目演示一個(gè)檢測(cè)用戶(hù)端是否是移動(dòng)端的例子。比如,你通過(guò)搜索引擎搜索到一個(gè)網(wǎng)頁(yè),然后打開(kāi)網(wǎng)頁(yè),當(dāng)然,進(jìn)入的可能不是首頁(yè),也可能是首頁(yè)。如果當(dāng)用戶(hù)進(jìn)入時(shí),該網(wǎng)站能根據(jù)用戶(hù)的客戶(hù)端類(lèi)型,為用戶(hù)提供更好的服務(wù),那么該用戶(hù)可能就會(huì)發(fā)展成為該產(chǎn)品的最終用戶(hù)。那么問(wèn)題來(lái)了,怎么根據(jù)用戶(hù)的客戶(hù)端類(lèi)型為他提供更好的服務(wù)呢?請(qǐng)看以下流程圖:
項(xiàng)目目錄見(jiàn)下圖(源碼大家可以通過(guò)上面的鏈接拿到):
詳細(xì)代碼就不在這里浪費(fèi)地方貼出來(lái)了,感興趣的可以去下載源碼學(xué)習(xí),這里只貼一部分比較核心的代碼。
創(chuàng)建HttpModule
首先要?jiǎng)?chuàng)建自己的HttpModule,然后實(shí)現(xiàn)IHttpModule接口,默認(rèn)要實(shí)現(xiàn)Init和Dispose方法:
public class DetectMobileModule:IHttpModule{public void Dispose(){throw new NotImplementedException();}public void Init(HttpApplication context){context.BeginRequest += context_BeginRequest;}}在這個(gè)例子中,我們不需要在Dispose方法中寫(xiě)任何代碼,因?yàn)槲覀冞@個(gè)例子沒(méi)有使用任何要求釋放的資源(如FileStream或者SqlConnection等GC沒(méi)有處理的資源)。ASP.NET HttpModule在每個(gè)Http請(qǐng)求都會(huì)運(yùn)行,傳入到Init方法的HttpApplication上下文參數(shù)給具體的邊界調(diào)用提供了一些事件。這個(gè)例子中,我們只對(duì)BeginRequest邊界事件感興趣,它的代碼如下:
void context_BeginRequest(object sender, EventArgs e) {}context_BeginRequest中的代碼會(huì)在頁(yè)面執(zhí)行之前運(yùn)行,因此,這也就是我們可以檢測(cè)用戶(hù)是否是移動(dòng)端的地方。
檢測(cè)移動(dòng)端用戶(hù)
創(chuàng)建一個(gè)MobileDetect類(lèi),假設(shè)APP可用的有3大平臺(tái):Android,IOS和Windows 10 Mobile。這里檢測(cè)用戶(hù)客戶(hù)端類(lèi)型的方式很簡(jiǎn)單,看UserAgent是否包含確定的關(guān)鍵字即可。代碼如下:
public class MobileDetect {readonly HttpRequest _request;public MobileDetect(HttpContext context){_request = context.Request;}public bool IsMobile(){return _request.Browser.IsMobileDevice&&(IsWindowsMobile()||IsAndroid()||IsApple());}/// <summary>/// 檢測(cè)是否是Windows Mobile手機(jī),本人在調(diào)試時(shí)發(fā)現(xiàn),Windows 10 Mobile系統(tǒng)的UserAgent同時(shí)包含了下面的兩個(gè)關(guān)鍵字/// </summary>/// <returns></returns>public bool IsWindowsMobile(){return _request.UserAgent.Contains("Windows Phone") && _request.UserAgent.Contains("Android");}public bool IsApple() {return _request.UserAgent.Contains("iPhone") || _request.UserAgent.Contains("iPad");}public bool IsAndroid(){return _request.UserAgent.Contains("Android") && !_request.UserAgent.Contains("Windows Phone");}}重定向到插入頁(yè)
接下來(lái),我們要在context_BeginRequest事件句柄中使用上面定義的MobileDetect類(lèi)了。如果MobileDetect類(lèi)檢測(cè)到用戶(hù)的請(qǐng)求來(lái)自智能手機(jī),那么他會(huì)被重定向到一個(gè)插入頁(yè)MobileInterstitial.aspx:
void context_BeginRequest(object sender, EventArgs e) {var context = HttpContext.Current;//使用當(dāng)前上下文對(duì)象創(chuàng)建一個(gè)MobileDetect對(duì)象var mobileDetect=new MobileDetect(context);if (mobileDetect.IsMobile()){//如果用戶(hù)拒絕下載APP,那么我們需要將他跳轉(zhuǎn)回之前訪問(wèn)的頁(yè)面var url = context.Request.RawUrl;var encodeUrl = HttpUtility.UrlEncode(url);//重定向到下載插入頁(yè),并帶上returnUrl,以防用戶(hù)需要返回到之前的頁(yè)面context.Response.Redirect("MobileInterstitial.aspx?returnUrl=" + encodeUrl);} }插入頁(yè)效果很簡(jiǎn)單,如下所示:
兩個(gè)按鈕的點(diǎn)擊事件如下:
/// <summary> /// “不,謝謝”的按鈕點(diǎn)擊事件,用戶(hù)點(diǎn)擊了該按鈕之后,需要將用戶(hù)導(dǎo)向之前訪問(wèn)的url /// </summary> /// <param name="sender"></param> /// <param name="e"></param> protected void btnThanks_Click(object sender, EventArgs e){//取到上一次請(qǐng)求的urlvar url = Request.QueryString.Get("returnUrl");Response.Redirect(HttpUtility.UrlDecode(url));} /// <summary> /// 點(diǎn)擊下載按鈕之后,跳轉(zhuǎn)到相應(yīng)的應(yīng)用市場(chǎng) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> protected void btnDownload_Click(object sender, EventArgs e){var mobileDetect=new MobileDetect(Context);if (mobileDetect.IsAndroid()){Response.Redirect("http://s1.music.126.net/download/android/CloudMusic_official_3.6.0.143673.apk");}if (mobileDetect.IsApple()){Response.Redirect("https://itunes.apple.com/app/id590338362");}if (mobileDetect.IsWindowsMobile()){Response.Redirect("https://www.microsoft.com/store/apps/9nblggh6g0jf");}}添加檢查
細(xì)心的園友可能會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,如果按照上面的代碼就這樣完了,那是會(huì)出問(wèn)題的。用戶(hù)的每次請(qǐng)求都會(huì)經(jīng)過(guò)HttpModule,這么一來(lái),每次請(qǐng)求都會(huì)檢測(cè)用戶(hù)的客戶(hù)端類(lèi)型,然后再次跳轉(zhuǎn)到插入下載頁(yè)。即使用戶(hù)點(diǎn)擊了“不,謝謝!”按鈕,還是會(huì)每次都跳轉(zhuǎn)到下載插入頁(yè)。這會(huì)讓用戶(hù)感到很煩人,可能會(huì)立即關(guān)閉這個(gè)網(wǎng)頁(yè),因而我們也就失去了一個(gè)潛在用戶(hù)。因此,我們需要在context_BeginRequest中添加條件判斷:
void context_BeginRequest(object sender, EventArgs e){//如果上一次請(qǐng)求來(lái)自下載插入頁(yè)或者當(dāng)前請(qǐng)求就是下載插入頁(yè),那么直接返回if (ComingFromMobileInterstitial()||OnMobileInterstitial()){return;}var context = HttpContext.Current;//使用當(dāng)前上下文對(duì)象創(chuàng)建一個(gè)MobileDetect對(duì)象var mobileDetect=new MobileDetect(context);if (mobileDetect.IsMobile()){//如果用戶(hù)拒絕下載APP,那么我們需要將他跳轉(zhuǎn)回之前訪問(wèn)的頁(yè)面var url = context.Request.RawUrl;var encodeUrl = HttpUtility.UrlEncode(url);//重定向到下載插入頁(yè),并帶上returnUrl,以防用戶(hù)需要返回到之前的頁(yè)面context.Response.Redirect("MobileInterstitial.aspx?returnUrl=" + encodeUrl);}}/// <summary>/// 檢查當(dāng)前請(qǐng)求的前一次請(qǐng)求是否是來(lái)自下載插入頁(yè)/// </summary>/// <returns></returns>bool ComingFromMobileInterstitial(){var request = HttpContext.Current.Request;if (request.UrlReferrer==null){return false;}return request.UrlReferrer.AbsoluteUri.Contains("MobileInterstitial.aspx");}/// <summary>/// 判斷當(dāng)前請(qǐng)求是不是包含插入頁(yè)文件/// </summary>/// <returns></returns>bool OnMobileInterstitial(){var request = HttpContext.Current.Request;return request.RawUrl.Contains("MobileInterstitial.aspx");}上面只是解決了當(dāng)用戶(hù)點(diǎn)擊拒絕下載之后用戶(hù)不會(huì)再次直接跳轉(zhuǎn)到下載插入頁(yè)的問(wèn)題,用戶(hù)就不會(huì)卡在這個(gè)死循環(huán)了。但是我們還可以做得更好,假設(shè)用戶(hù)不想安裝APP,并希望在一個(gè)正常的移動(dòng)端瀏覽器中查看頁(yè)面,而且,用戶(hù)點(diǎn)擊了拒絕下載按鈕之后,也不要每次請(qǐng)求都要重定向到下載插入頁(yè)。
進(jìn)一步完善
當(dāng)用戶(hù)點(diǎn)擊了“不,謝謝!”按鈕之后,我們就不要在每次頁(yè)面請(qǐng)求時(shí)都跳轉(zhuǎn)到下載插入頁(yè),不要再打擾他們了。一種方式就是當(dāng)用戶(hù)點(diǎn)擊了該按鈕之后,設(shè)置一個(gè)cookie:
protected void btnThanks_Click(object sender, EventArgs e) {//用戶(hù)點(diǎn)擊拒絕下載按鈕之后,設(shè)置一個(gè)cookie,并根據(jù)自己的情況設(shè)置一個(gè)有效期,這里為了演示,設(shè)置為2分鐘var cookie=new HttpCookie("NoThanks","yes");cookie.Expires = DateTime.Now.AddMinutes(2);Response.Cookies.Add(cookie);//取到上一次請(qǐng)求的urlvar url = Request.QueryString.Get("returnUrl");Response.Redirect(HttpUtility.UrlDecode(url)); }接下來(lái),我們需要在context_BeginRequest方法中檢查是否具有特定值的Cookie,從而是否將用戶(hù)重定向到下載插入頁(yè):
void context_BeginRequest(object sender, EventArgs e) {//如果請(qǐng)求中的Cookie包含NoThanks鍵或者上一次請(qǐng)求來(lái)自下載插入頁(yè)或者當(dāng)前請(qǐng)求就是下載插入頁(yè),那么直接返回if (ExistNoThanksCookie()||ComingFromMobileInterstitial()||OnMobileInterstitial()){return;}var context = HttpContext.Current;//使用當(dāng)前上下文對(duì)象創(chuàng)建一個(gè)MobileDetect對(duì)象var mobileDetect=new MobileDetect(context);if (mobileDetect.IsMobile()){//如果用戶(hù)拒絕下載APP,那么我們需要將他跳轉(zhuǎn)回之前訪問(wèn)的頁(yè)面var url = context.Request.RawUrl;var encodeUrl = HttpUtility.UrlEncode(url);//重定向到下載插入頁(yè),并帶上returnUrl,以防用戶(hù)需要返回到之前的頁(yè)面context.Response.Redirect("MobileInterstitial.aspx?returnUrl=" + encodeUrl);} }bool ExistNoThanksCookie(){return HttpContext.Current.Request.Cookies.Get("NoThanks") != null;}下面樓主將網(wǎng)站發(fā)布到IIS,使用Windows 10 Mobile,借助Windows 10 PC RS1版的連接功能,給大家截取動(dòng)態(tài)圖演示一下效果,其他類(lèi)型的手機(jī)也可以訪問(wèn)網(wǎng)站并跳轉(zhuǎn)到對(duì)應(yīng)的應(yīng)用商店,但是樓主這里主要可以借助win10 PC和手機(jī)進(jìn)行投影給大家演示效果。動(dòng)態(tài)圖很大的,近1000幀剪輯得還剩100多幀。
Web應(yīng)用中的HttpModule使用AOP很好地解決了橫切關(guān)注點(diǎn)的問(wèn)題,別忘了我們這個(gè)系列的目的,是學(xué)習(xí)AOP的,而不是Web開(kāi)發(fā)中的一些細(xì)節(jié)知識(shí)點(diǎn),這個(gè)例子是頁(yè)面邊界切面的例子,下面我們看一個(gè)PostSharp方法邊界處理緩存的例子。
真實(shí)案例——緩存
在web開(kāi)發(fā)中有一種數(shù)據(jù)庫(kù)優(yōu)化的方法,比如,一個(gè)頁(yè)面可能調(diào)用了很多次數(shù)據(jù)庫(kù),那么這些調(diào)用可以通過(guò)優(yōu)化代碼和減少數(shù)據(jù)庫(kù)調(diào)用來(lái)改善性能。但是有時(shí)處理的速度不是我們能控制的,比如某些處理過(guò)程真的很復(fù)雜,需要花費(fèi)很多時(shí)間來(lái)處理;有時(shí)我們需要依賴(lài)外部的處理(數(shù)據(jù)庫(kù),web服務(wù)等等),這些我們幾乎沒(méi)有控制權(quán)。
重點(diǎn)來(lái)了,如果需要的數(shù)據(jù)處理的很慢,并且這些數(shù)據(jù)不經(jīng)常變化,那么我們可以使用緩存來(lái)減少等待時(shí)間。Caching通常對(duì)于多用戶(hù)的系統(tǒng)是非常有利的,第一次的請(qǐng)求還是很慢的,然后緩存將第一次請(qǐng)求的結(jié)果存儲(chǔ)到可以迅速讀取數(shù)據(jù)的本地,之后其他的請(qǐng)求就會(huì)先去緩存檢測(cè)是否有需要的數(shù)據(jù),如果有的話,就會(huì)直接從緩存中取出數(shù)據(jù),從而跳過(guò)緩慢的處理過(guò)程。
緩存也可以看作是一個(gè)橫切關(guān)注點(diǎn),對(duì)于想要使用緩存的每個(gè)方法,可以按照以下步驟來(lái):
用流程圖畫(huà)一下:
上面的流程在代碼中都實(shí)現(xiàn)出來(lái)的話,可能會(huì)導(dǎo)致大量的樣板代碼,這就暗示我們使用AOP是個(gè)不錯(cuò)的主意。下面我們看一個(gè)ASP.NET中關(guān)于Cache對(duì)象的例子,并編寫(xiě)一個(gè)切面來(lái)更有效的工作。
ASP.NET Cache
不同類(lèi)型的應(yīng)用可以使用不同的緩存工具,比如NCache,Memcached等。但這里我們關(guān)注的是如何使用AOP處理緩存而不是各種緩存工具的使用,下面的例子會(huì)使用.Net開(kāi)發(fā)者的老朋友 ASP.NET Cache。
ASP.NET代碼中的緩存就像一個(gè)可以使用的字典對(duì)象,在ASP.NET WEB Froms中,Cache繼承自Page基類(lèi),而在ASP.NET MVC中,通過(guò)繼承自Controller基類(lèi)的HttpContext就可以使用緩存了。如果上面的都無(wú)法讀取緩存,可以通過(guò)HttpContext.Current.Cache獲取。
Cache對(duì)象的API很簡(jiǎn)單,可以把它當(dāng)作字典來(lái)使用,可以從Cache中獲取值,也可以往Cache中添加值。如果要獲取的值沒(méi)有存在于緩存中,就會(huì)返回null。
Cache["MyCacheKey"] = "some value";//使用MyCacheKey作為鍵存儲(chǔ)some value var myValue = Cache["MyCacheKey"];//使用鍵獲取緩存 var myValue = Cache["SomeOtherKey"];//如果緩存不存在就會(huì)返回nullCache還有很多有用的其他方法,比如Add 和 Insert方法,這可以讓我們指定緩存的過(guò)期時(shí)間。此外,也可以使用Remove方法立即從緩存中移除一個(gè)值。
Cache 有效期
緩存值通常都會(huì)設(shè)置一個(gè)過(guò)期時(shí)間。比如,如果使用"CacheKey"存儲(chǔ)了一個(gè)值,并設(shè)置過(guò)期時(shí)間是2小時(shí)之后,那么2小時(shí)之后,使用"CacheKey"檢索那個(gè)值時(shí)就會(huì)返回null。
ASP.NET Cache有幾個(gè)可以使用的過(guò)期時(shí)間設(shè)置:
關(guān)于緩存的一個(gè)案例
這次我們創(chuàng)建一個(gè)ASP.NET MVC項(xiàng)目,項(xiàng)目的目錄結(jié)構(gòu)如下:
上面的其他文件夾Content,Scripts,Controller,Models等等就不用多說(shuō)了,不懂的話,請(qǐng)去學(xué)習(xí)ASP.NET MVC。下面在用一張動(dòng)態(tài)圖看一下整個(gè)網(wǎng)站的效果:
這個(gè)項(xiàng)目是樓主從頭搭建起來(lái)的,整體布局使用的是法拉利紅作為主題色,雖然給自己的定位是全棧,但是整個(gè)頁(yè)面的布局還是花了不少時(shí)間的,看來(lái)自己還得在css和html方面深入學(xué)習(xí)一下啊。放了三個(gè)導(dǎo)航鏈接,Home頁(yè)隨便找了一輛自己看著還不錯(cuò)的法拉利圖片,About放了兩張打賞的圖片,其實(shí)要講的東西在最后一個(gè)Value頁(yè)面。
和之前一樣,css,html,js代碼這里就不貼出來(lái)了,感興趣的可以去看源碼,這里只放一些關(guān)于AOP的核心代碼。
Value顯示頁(yè)面
下面是點(diǎn)擊Value按鈕時(shí)的Action代碼,主要是放了些select中的數(shù)據(jù)和讀取緩存內(nèi)容:
[HttpGet] public ActionResult Value() {ViewData["Cache"]= DisplayCache();//顯示緩存內(nèi)容//制造商數(shù)據(jù)var makes = new SelectList(new List<SelectListItem>{new SelectListItem{Text = "法拉利",Value = "Ferrari",Selected = true},new SelectListItem{Text = "勞斯萊斯",Value = "Rolls-Royce"},new SelectListItem{Text = "邁巴赫",Value = "Maybach"}},"Value","Text");//年份數(shù)據(jù)var years=new SelectList(new List<SelectListItem>{new SelectListItem{Text = "2014年",Value = "2014"},new SelectListItem{Text = "2015年",Value = "2015"},new SelectListItem{Text = "2016年",Value = "2016",Selected = true}},"Value","Text");//條件數(shù)據(jù)var conditions=new SelectList(new List<SelectListItem>{new SelectListItem{Text = "經(jīng)濟(jì)型",Value = "poor",Selected = true},new SelectListItem{Text = "舒適型",Value = "comfort"},new SelectListItem{Text = "豪華型",Value = "best"}},"Value","Text");ViewData["makes"] = makes;ViewData["years"] = years;ViewData["conditions"] = conditions;return View(); }/// <summary> /// 顯示緩存內(nèi)容 /// </summary> /// <returns></returns> private List<string> DisplayCache() {var cacheList=new List<string>();//Response.Cache.SetCacheability(HttpCacheability.NoCache);//Response.Cache.SetExpires(DateTime.Now.AddYears(-2));//ClearAllCache();foreach (DictionaryEntry cache in HttpContext.Cache){cacheList.Add(string.Format("{0}-{1}",cache.Key,cache.Value));}if (!cacheList.Any()){cacheList.Add("None");}return cacheList; }看到緩存里面有很多不知哪里生成的東西,就寫(xiě)了個(gè)ClearAllCache()方法清除所有的緩存,但是這樣就沒(méi)辦法把自己的緩存也清除了,所以這里注釋了。這里也不貼實(shí)現(xiàn)了,感興趣的話請(qǐng)看源碼。
獲取Value的Action
選擇好各個(gè)條件之后,點(diǎn)擊獲取Value 按鈕就會(huì)通過(guò)ajax異步將選擇的條件提交到下面這個(gè)action:
[HttpPost] public ActionResult ValuePost(FormCollection collection) {var years = Convert.ToInt32(Request.Form.Get("years"));var makes = Request.Form.Get("makes");var conditions = Request.Form.Get("conditions");//第二種方式獲取form表單的值//var years2 = Convert.ToInt32(collection.Get("years"));//var makes2 = collection.Get("makes");//var conditions2 = collection.Get("conditions");var carValueService=new CarValueService();//第一種方式獲取汽車(chē)價(jià)格,不具有健壯性,故不采用//var value = carValueService.GetValue(years, makes, conditions);var value = carValueService.GetValueBetter(new CarValueArgs{Condition = conditions,Make = makes,Year = years});return Content(value.ToString("c")); }這個(gè)action就取到前端傳過(guò)來(lái)的條件參數(shù),然后使用這些參數(shù)借助CarValueService服務(wù)類(lèi)獲得車(chē)輛的價(jià)格。
CarValueService服務(wù)類(lèi)
下面是一個(gè)汽車(chē)服務(wù)類(lèi),一般情況下,這些數(shù)據(jù)是第三方汽車(chē)廠商或代理商、分銷(xiāo)商等提供的,變化頻率不是很高,而且調(diào)用一個(gè)Web Service可能會(huì)很慢,因此,可以用戶(hù)緩存處理。這里我們使用Thread.Sleep(5000);來(lái)模擬一個(gè)耗時(shí)操作。這里有兩個(gè)方法,一個(gè)是GetValue,一個(gè)是GetValueBetter,上面也已經(jīng)說(shuō)了,后面的方法健壯性更好,因?yàn)橹恍枰姆?wù)類(lèi)方法的參數(shù)的屬性就夠了,而不用修改服務(wù)類(lèi)方法的參數(shù)的簽名。
public class CarValueService {readonly Random _ran;public CarValueService(){_ran=new Random();}[CacheAspect]public decimal GetValue(int year,string makeId,string conditionId){Thread.Sleep(5000);return _ran.Next(1000000, 10000000);}[CacheAspect]public decimal GetValueBetter(CarValueArgs args){Thread.Sleep(5000);return _ran.Next(1000000, 10000000);} }汽車(chē)的價(jià)格這里是去獲取100w到1000w之間的隨機(jī)數(shù)。方法上面都使用了緩存切面CacheAspect特性。
緩存切面CacheAspect
既然是調(diào)用第三方不頻繁變化的數(shù)據(jù),那么就可以把請(qǐng)求的結(jié)果緩存起來(lái)。
[Serializable] public class CacheAspect : OnMethodBoundaryAspect {/// <summary>/// 進(jìn)入方法前執(zhí)行的邊界方法,進(jìn)入服務(wù)類(lèi)方法前先檢測(cè)一下緩存中是否有數(shù)據(jù),有就直接返回緩存中的數(shù)據(jù)/// </summary>/// <param name="args"></param>public override void OnEntry(MethodExecutionArgs args){var key = GetCacheKeyBetter(args);if (HttpContext.Current.Cache[key] == null){return;//退出OnEntry方法,繼續(xù)執(zhí)行服務(wù)類(lèi)方法}args.ReturnValue = HttpContext.Current.Cache[key];args.FlowBehavior = FlowBehavior.Return;//這里的Return指的是跳過(guò)服務(wù)類(lèi)方法}/// <summary>/// 方法成功執(zhí)行后執(zhí)行的邊界方法,調(diào)用第三方服務(wù)成功后緩存獲取的結(jié)果/// </summary>/// <param name="args"></param>public override void OnSuccess(MethodExecutionArgs args){//var key = GetCacheKey(args);var key = GetCacheKeyBetter(args);HttpContext.Current.Cache[key] = args.ReturnValue;}/// <summary>/// 獲取Cache鍵,對(duì)應(yīng)服務(wù)類(lèi)方法有多個(gè)參數(shù)的版本/// </summary>/// <param name="args"></param>/// <returns></returns>private string GetCacheKey(MethodExecutionArgs args){var contactArgs = string.Join("_", args.Arguments);contactArgs = args.Method.Name + "-" + contactArgs;return contactArgs;}/// <summary>/// 獲取Cache鍵,升級(jí)版本,對(duì)應(yīng)服務(wù)類(lèi)方法只有一個(gè)對(duì)象參數(shù)/// </summary>/// <param name="args"></param>/// <returns></returns>private string GetCacheKeyBetter(MethodExecutionArgs args){//方法1:通過(guò)JsonConvert//var jsonArr = args.Arguments.Select(JsonConvert.SerializeObject).ToArray();var jsonArr = args.Arguments.Select(new JavaScriptSerializer().Serialize).ToArray();return args.Method.Name+"_" + string.Join("_", jsonArr);} }上面的代碼已經(jīng)解釋地很清楚了,大家看代碼注釋就好。
這里為什么將緩存的鍵加入Json?
易讀。當(dāng)看到屏幕上緩存的內(nèi)容時(shí),很清楚知道發(fā)生了什么,以及緩存了什么。
輕量。無(wú)意冒犯xml粉,但這里真不需要額外的XML頭和其他命名空間信息等標(biāo)簽。
易生成。使用JsonConvert類(lèi)或JavaScriptSerializer就可以輕易搞定。
其實(shí)這里選哪種方式序列化無(wú)所謂,只要能實(shí)現(xiàn)給緩存生成一個(gè)唯一的鍵的目的就行。
小結(jié)
這篇博文我們看了一下切面常用的類(lèi)型:邊界切面。代碼中的邊界就像國(guó)家之間的分界線一樣,它給我們提供了將行為放到代碼邊界的機(jī)會(huì)。兩個(gè)常見(jiàn)的例子就是web頁(yè)面加載前后和方法調(diào)用前后的例子。跟方法攔截切面一樣,邊界切面提供了封裝橫切關(guān)注點(diǎn)的另一種方式。
PostSharp提供了編寫(xiě)方法攔截切面的能力,ASP.NET通過(guò)HttpModule提供了編寫(xiě)Web頁(yè)面邊界的能力,而且他們的API都提供了上下文信息(比如Http請(qǐng)求和方法的信息),以及控制程序流的能力(比如重定向頁(yè)面或立即從方法返回)。
這篇博客還做了好幾個(gè)示例,希望正在看博客的你能自己動(dòng)手實(shí)踐一下。
轉(zhuǎn)載于:https://www.cnblogs.com/farb/p/BoundaryAspects.html
總結(jié)
以上是生活随笔為你收集整理的.Net中的AOP系列之《方法执行前后——边界切面》的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: DIY厂商排队卖惨:显卡卖不动 内存要降
- 下一篇: 精致堪比MacBook Pro!小米笔记