.Net中的AOP系列之《方法執行前后——邊界切面》

返回《.Net中的AOP》系列學習總目錄


本篇目錄

  • 邊界切面
    • PostSharp方法邊界
    • 方法邊界 VS 方法攔截
    • ASP.NET HttpModule邊界
  • 真實案例——檢查是否為移動端用戶
  • 真實案例——緩存
  • 小結

本系列的源碼本人已托管于Coding上:點擊查看

本系列的實驗環境:VS 2013 Update 5(建議最好使用集成了Nuget的VS版本,VS Express版也夠用),安裝PostSharp。

這篇博客覆蓋的內容包括:

  • 什么是方法邊界
  • 使用PostSharp的邊界方法
  • 編寫ASP.NET HttpModule來檢測用戶是否是移動端用戶
  • 方法攔截和方法邊界的不同之處
  • 使用PostSharp編寫緩存切面

上一篇我們講了方法切面中最通用的類型《方法攔截》,這篇我們講一下可能用到的另外一種切面:邊界切面,它里面的代碼會在方法的邊界運行。首先會使用PostSharp在方法層面上演示一個邊界切面,然后也會使用ASP.NET HttpModule演示一個頁面層面的邊界切面。

這篇博客的目的是通過多個例子演示來說明什么事邊界切面以及邊界切面一般是如何工作的,而不是帶領大家詳細地編寫這些例子。

邊界切面

通常意義上的邊界指的是兩個實體間的任意分割線,比如兩個國家之間的地理上的分界線,我國各個省份之間的分界線,當你去臨省旅游時,你必須首先穿過你所在省和鄰省的分界線,旅行結束返回時,必須再次穿過省份分界線。

和現實生活一樣,編碼時也會有很多分界線。就拿最簡單的控制臺程序來說,當啟動一個Main方法時,然后Main方法又調用了另一個方法,當程序進入被調用方法體時也要穿過一個分界線,當被調方法執行完成之后,程序流就會返回到Main方法,這就是我們平時沒怎么意識到的邊界。

使用了AOP,我們就可以把代碼放到那些邊界上,這些邊界代表了一個地方或一個條件,對于放置一些可復用的代碼很有用。

PostSharp方法邊界

創建一個控制臺項目,名為“BasketballStatsPostSharp”,解決方案名稱為"BoundaryAspectsPractices",通過Nuget安裝PostSharp。這個項目的需求很簡單,創建一個服務類,然后根據球員的名字獲得該球員的球衣號碼,這里為了演示,直接將結果打印到控制臺。


public class BasketballStatsService
{/// <summary>/// 根據球員的名字返回球員的球衣號碼/// </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){//這個花括號是程序沒有執行和開始執行的分界線var service=new BasketballStatsService();var playName = "Michael Jordan";var no1 = service.GetPlayerNumber(playName);//這里是Main方法和GetPlayerNumber方法的分界線Console.WriteLine("{0}的球衣號碼是{1}",playName,no1);Console.Read();}//這個花括號是程序結束前和程序結束后的分界線
}

這只是個普通的程序,沒什么可言之處,大家很容易看出運行結果,這里就不演示了。
下面我們創建一個邊界切面MyBoundaryAspect,它繼承自PostSharp中的OnMethodBoundaryAspect,注意使用PostSharp時記得使用Serializable特性。

[Serializable]
public class MyBoundaryAspect:OnMethodBoundaryAspect
{public override void OnEntry(MethodExecutionArgs args){Console.WriteLine("方法{0}執行前",args.Method.Name);}public override void OnSuccess(MethodExecutionArgs args){Console.WriteLine("方法{0}執行后", args.Method.Name);}
}

使用的話,很簡單,只需要在服務類的方法上加上特性即可,然后運行如下:

圖片

這個例子和第一篇介紹中的"Hello World"例子差不多,沒什么好玩的,別著急,在本文后面會有一個使用邊界方法處理緩存的例子。

方法邊界 VS 方法攔截

目前,方法邊界切面和方法攔截切面我們都看過了,那么接下來對比一下這兩者有什么區別。區別肯定是存在的,但這些區別是很微妙的,專一的開發者可能只使用其中一種切面。這節從下面兩個方面討論一下這些區別:

  1. 切面方法間的共享狀態
  2. 代碼清晰度/意圖

下圖是PostSharp中MethodInterceptionAspectOnMethodBoundary切面的基本結構對比:

圖片

概念上講,可以將一個邊界切面轉成攔截切面,反之亦然,只需要將左邊的代碼改為右邊格式的代碼就好了,但是,如果真那么簡單,那么這兩者之間的區別是什么呢?很明顯,答案肯定不是想象的那么簡單。

切面方法間的共享狀態

首先看一下共享狀態。攔截切面只有一個方法OnInvoke,因此共享狀態不是關心的問題——在方法開始時可以使用的任何變量可以繼續在方法的其他地方使用。但是對于邊界方法來說就不那么簡單了,在OnEntry方法中聲明的變量在OnSuccess方法中是不可用的,因為它們是分離的方法。

但使用PostSharp,對于邊界方法的共享狀態可以變通一下。首先,可以使用類本身的字段:

 [Serializable]public class MyBoundaryAspect:OnMethodBoundaryAspect{private string _sharedState;//使用一個全局變量共享方法之間的信息public override void OnEntry(MethodExecutionArgs args){_sharedState = "123";//邊界方法運行之前,設置一個值Console.WriteLine("方法{0}執行前",args.Method.Name);}public override void OnSuccess(MethodExecutionArgs args){Console.WriteLine("方法{0}執行后,_sharedState={1}", args.Method.Name,_sharedState);//邊界方法運行之后該值不變}}

然而,這種方法有個缺點。在PostSharp中,切面類中的每個邊界方法都使用切面類的相同實例。這種切面叫做靜態范圍切面,這意味著,即使你創建了多個類的實例,PostSharp的切面標記的方法只會創建一個切面實例與那個類對應。如果切面實現了IInstanceScopedAspect接口,那么這個切面就是一個實例范圍切面。默認行為會在編織之后,在代碼中添加少量負擔,但是引入的那點復雜度可能不是很明顯。

要演示這個問題,修改一下切面類和Main方法,服務類方法不變,代碼修改如下:

 [Serializable]public class MyBoundaryAspect:OnMethodBoundaryAspect{private readonly Guid _sharedState;//使用一個全局變量共享方法之間的信息public MyBoundaryAspect(){_sharedState = Guid.NewGuid();}public override void OnEntry(MethodExecutionArgs args){//_sharedState = "123";//邊界方法運行之前,設置一個值Console.WriteLine("方法{0}執行前",args.Method.Name);}public override void OnSuccess(MethodExecutionArgs args){Console.WriteLine("方法{0}執行后,_sharedState={1}", args.Method.Name,_sharedState);//邊界方法運行之后該值不變}}#region 攔截切面VS邊界切面var s1=new BasketballStatsService();var s2=new BasketballStatsService();s1.GetPlayerNumber("Kobe Bryant");s2.GetPlayerNumber("Kobe Bryant");#endregionConsole.Read();

運行效果如下:

圖片

從結果可以看到,產生的GUID的值是一樣的,也就是說,切面實例(每次實例化時都會產生)只產生了一個,也就是說多個服務類的方法共享了相同的MyBoundaryAspect切面對象。如果又調用了服務類的另外一個方法,那么生成的GUID的值就不同了。

GUID
GUID是Globally Unique Identifier(全局唯一標識符)的簡寫。GUID是用于唯一標識的128bit的值,通常表現為16進制的8-4-4-4-12形式。Guid.NewGuid()會生產一個唯一的Guid(不是從數學角度,而是從實際和統計角度),因此很適合演示產生的實例是不是同一個實例。

總之,切面的全局字段不是切面方法間溝通的安全方式,因為它不是線程安全的。其他方法可以對這些全局字段更改,因此,PostSharp提供了一個叫做args.MethodExecutionTag的API來協助共享狀態。它是會傳入每個邊界方法的args對象的屬性,該對象對于方法調用時的每次特定時間都是唯一的。

現在,將Guid.NewGuid()移到構造函數的外面的OnEntry方法中,然后在OnSuccess方法中使用args.MethodExecutionTag方式輸出。代碼如下:

[Serializable]
public class MyBoundaryAspect:OnMethodBoundaryAspect
{private readonly Guid _sharedState;//使用一個全局變量共享方法之間的信息public MyBoundaryAspect(){// _sharedState = Guid.NewGuid();}public override void OnEntry(MethodExecutionArgs args){//_sharedState = "123";//邊界方法運行之前,設置一個值args.MethodExecutionTag = Guid.NewGuid();Console.WriteLine("方法{0}執行前,該方法生成的Guid={1}",args.Method.Name,args.MethodExecutionTag);}public override void OnSuccess(MethodExecutionArgs args){//Console.WriteLine("方法{0}執行后,_sharedState={1}", args.Method.Name,_sharedState);//邊界方法運行之后該值不變Console.WriteLine("方法{0}執行后,該方法生成的Guid={1}", args.Method.Name, args.MethodExecutionTag);}
}

運行結果如下:

圖片

從上面的運行結果看以看出,同一個邊界切面中的不同邊界方法共享了相同的數據GUID,但是不同的服務類實例調用使用了同一個切面的方法,GUID是不同的。

MethodExecutionTag是一個對象類型,適合存儲一些像GUID等簡單的類型,如果需要存儲更復雜的共享數據,必須在使用時強制轉換MethodExecutionTag的類型。如果要存儲一個包含了多個對象的共享數據,必須創建一個自定義類存儲到MethodExecutionTag屬性中。

記住,方法攔截切面中不存在這些問題,因為OnInvoke方法是方法攔截切面中唯一的方法,可以在該方法中使用所有的共享數據。上一篇例子中的數據事務就是一個使用了很多共享數據的例子,比如重試次數的數量,事務是否成功執行的標識succeeded都是共享數據。

那么如何選擇何時使用攔截切面還是邊界切面呢?方法是:如果你要編寫的切面使用了復雜的共享數據,或者使用了很多共享數據,那么最好使用方法攔截切面。

代碼清晰度/意圖

方法攔截切面在數據共享方法有明顯的優勢,但沒有共享數據或者共享數據很少呢?或者需要在某個單獨的邊界執行一些代碼呢?這些場合,方法邊界切面更勝一籌。

下面寫一個切面,該切面運行在方法完成時的邊界(無論方法是否成功)。在PostSharp中需要編寫這個邊界切面,
需要重寫OnExit方法,它不同于OnSuccess方法,后者只有當方法沒有拋出異常執行完畢時才會執行,而前者當方法執行完成時都會運行,不管有沒有拋異常都會執行。

//邊界切面的寫法
public override void OnExit(MethodExecutionArgs args)
{Console.WriteLine("方法{0}執行完成!",args.Method.Name);
}

如果要在攔截切面中寫的話,就需要這么寫:


public class MyIntercepor : MethodInterceptionAspect
{public override void OnInvoke(MethodInterceptionArgs args){try{args.Proceed();//在邊界切面中,這行代碼是隱式執行的}finally //C#中的finally指的是,無論try中發生了什么,代碼塊都會執行{Console.WriteLine("方法{0}執行完成!", args.Method.Name);}}
}

上面這個例子很簡單,但是現實中的項目不可能這么簡單,可能try和finally代碼塊中的代碼都很多,那么此時使用攔截切面維護就顯得更加費力,因為第一眼看得代碼更多,而且代碼一多,可能發生的問題更多。而邊界切面隱藏了try/catch/finallyProceed()的細節,我們不需要讀寫那些代碼。

最后要說的是,雖然你可能偏愛方法攔截,但不要忽略了邊界切面,因為它可以改善代碼的清晰度和簡潔度。

性能和內存考慮

方法邊界切面和方法攔截切面其他的重要區別是性能和內存方面,這些方法的考慮取決于使用的工具的不同而不同。
在PostSharp中,當使用MethodInterceptionAspect時,所有的參數每次都會從棧中復制到堆中(通過裝箱boxing),當使用OnMethodBoundaryAspect時,PostSharp會檢測沒有使用的參數,不會把這些參數裝箱,從而優化了代碼。因此,如果編寫的切面沒有使用方法參數,那么使用OnMethodBoundaryAspect會使用更少的內存,如果在多個地方都使用這個切面,那么這樣的做法可能是重要的(注意:該優化功能沒有包含在PostSharp的免費版中)。

方法邊界不是使用AOP時唯一有用的邊界類型,下面我們會看一個ASP.NET HttpModule的例子,這個例子對于把邊界放到web頁面上非常有用。

ASP.NET HttpModule邊界

這里為了方便演示,創建一個Asp.Net Web Form項目WebFormHttpModule,新建一個頁面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>這是一個Demo頁面!</h1></div></form>
</body>
</html>

在瀏覽器中瀏覽該文件時,頁面上會顯示這是一個Demo頁面!這句話。對每個ASP.NET 頁面的請求都會有一個很復雜的生命周期,但值得注意的是該生命周期中的一部分使用了HttpModule,它允許我們將代碼放到ASP.NET頁面的邊界。要創建一個HttpModule,需要創建一個實現了IHttpModule接口的類:

public class MyHttpModule:IHttpModule
{/// <summary>/// 釋放所有的資源和數據庫連接/// </summary>public void Dispose(){throw new NotImplementedException();}/// <summary>/// 當HttpApplication的實例創建時運行/// </summary>/// <param name="context"></param>public void Init(HttpApplication context){throw new NotImplementedException();}
}

每個模塊都必須在ASP.NET的Web.config文件中配置后才可以運行。Web.config的配置可能會根據你使用的web服務器( IIS6 , IIS7 +, Cassini, IIS Express等等)不同而不同。要想覆蓋以上服務器的所有配置,可以像下面那樣配置:

<!--II6和ASP.NET開發服務器會在這里尋找-->
<system.web><compilation debug="true" targetFramework="4.5" /><httpModules><!--每個模塊需要唯一的名字和類型(全命名空間+類名)--><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使用了多個工作進程處理即將到來的請求,每個工作進程都會創建一個HttpApplication的實例,每個HttpApplication實例都會創建一個HttpModule,然后運行Init方法,現在自定義的Init方法什么都還沒寫,下面會使用事件句柄設置一些邊界:

public class MyHttpModule:IHttpModule
{/// <summary>/// 釋放所有的資源和數據庫連接/// </summary>public void Dispose(){throw new NotImplementedException();}/// <summary>/// 當HttpApplication的實例創建時運行/// </summary>/// <param name="context"></param>public void Init(HttpApplication context){context.BeginRequest += context_BeginRequest;context.EndRequest += context_EndRequest;}/// <summary>/// 在所有的其他頁面生命周期事件結束之后運行/// </summary>/// <param name="sender"></param>/// <param name="e"></param>void context_EndRequest(object sender, EventArgs e){var app = sender as HttpApplication;app.Response.Write("頁面所有的生命周期事件結束之后");}/// <summary>/// 頁面處理請求之前運行/// </summary>/// <param name="sender"></param>/// <param name="e"></param>void context_BeginRequest(object sender, EventArgs e){var app = sender as HttpApplication;app.Response.Write("頁面請求處理之前");}
}

雖然語法很不同,但是這種感覺很像之前的方法邊界切面。瀏覽一下頁面,效果如下:

圖片

因為在這些邊界方法中有一個HttpApplication對象,因此可以有很大的靈活性和潛能完成很多事情。當檢查HttpApplication的屬性和事件時,可以看到做許多事情而不僅是輸出文本。下一節我們會使用HttpModule演示一個真實的案例:檢測用戶是否是移動端用戶。

真實案例——檢查是否為移動端用戶

下面再創建一個ASP.NET WebForm 項目演示一個檢測用戶端是否是移動端的例子。比如,你通過搜索引擎搜索到一個網頁,然后打開網頁,當然,進入的可能不是首頁,也可能是首頁。如果當用戶進入時,該網站能根據用戶的客戶端類型,為用戶提供更好的服務,那么該用戶可能就會發展成為該產品的最終用戶。那么問題來了,怎么根據用戶的客戶端類型為他提供更好的服務呢?請看以下流程圖:

577014-20160811000251543-1757923348.png

項目目錄見下圖(源碼大家可以通過上面的鏈接拿到):

577014-20160810235755184-1179802415.png

詳細代碼就不在這里浪費地方貼出來了,感興趣的可以去下載源碼學習,這里只貼一部分比較核心的代碼。

創建HttpModule

首先要創建自己的HttpModule,然后實現IHttpModule接口,默認要實現InitDispose方法:

public class DetectMobileModule:IHttpModule{public void Dispose(){throw new NotImplementedException();}public void Init(HttpApplication context){context.BeginRequest += context_BeginRequest;}}

在這個例子中,我們不需要在Dispose方法中寫任何代碼,因為我們這個例子沒有使用任何要求釋放的資源(如FileStream或者SqlConnection等GC沒有處理的資源)。ASP.NET HttpModule在每個Http請求都會運行,傳入到Init方法的HttpApplication上下文參數給具體的邊界調用提供了一些事件。這個例子中,我們只對BeginRequest邊界事件感興趣,它的代碼如下:

void context_BeginRequest(object sender, EventArgs e)
{}

context_BeginRequest中的代碼會在頁面執行之前運行,因此,這也就是我們可以檢測用戶是否是移動端的地方。

檢測移動端用戶

創建一個MobileDetect類,假設APP可用的有3大平臺:Android,IOS和Windows 10 Mobile。這里檢測用戶客戶端類型的方式很簡單,看UserAgent是否包含確定的關鍵字即可。代碼如下:

public class MobileDetect
{readonly HttpRequest _request;public MobileDetect(HttpContext context){_request = context.Request;}public bool IsMobile(){return _request.Browser.IsMobileDevice&&(IsWindowsMobile()||IsAndroid()||IsApple());}/// <summary>/// 檢測是否是Windows Mobile手機,本人在調試時發現,Windows 10 Mobile系統的UserAgent同時包含了下面的兩個關鍵字/// </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");}}

重定向到插入頁

接下來,我們要在context_BeginRequest事件句柄中使用上面定義的MobileDetect類了。如果MobileDetect類檢測到用戶的請求來自智能手機,那么他會被重定向到一個插入頁MobileInterstitial.aspx:

void context_BeginRequest(object sender, EventArgs e)
{var context = HttpContext.Current;//使用當前上下文對象創建一個MobileDetect對象var mobileDetect=new MobileDetect(context);if (mobileDetect.IsMobile()){//如果用戶拒絕下載APP,那么我們需要將他跳轉回之前訪問的頁面var url = context.Request.RawUrl;var encodeUrl = HttpUtility.UrlEncode(url);//重定向到下載插入頁,并帶上returnUrl,以防用戶需要返回到之前的頁面context.Response.Redirect("MobileInterstitial.aspx?returnUrl=" + encodeUrl);}
}

插入頁效果很簡單,如下所示:

577014-20160812232718093-572641433.png

兩個按鈕的點擊事件如下:

/// <summary>
/// “不,謝謝”的按鈕點擊事件,用戶點擊了該按鈕之后,需要將用戶導向之前訪問的url
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void btnThanks_Click(object sender, EventArgs e){//取到上一次請求的urlvar url = Request.QueryString.Get("returnUrl");Response.Redirect(HttpUtility.UrlDecode(url));}
/// <summary>
/// 點擊下載按鈕之后,跳轉到相應的應用市場
/// </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");}}

添加檢查

細心的園友可能會發現一個問題,如果按照上面的代碼就這樣完了,那是會出問題的。用戶的每次請求都會經過HttpModule,這么一來,每次請求都會檢測用戶的客戶端類型,然后再次跳轉到插入下載頁。即使用戶點擊了“不,謝謝!”按鈕,還是會每次都跳轉到下載插入頁。這會讓用戶感到很煩人,可能會立即關閉這個網頁,因而我們也就失去了一個潛在用戶。因此,我們需要在context_BeginRequest中添加條件判斷:

 void context_BeginRequest(object sender, EventArgs e){//如果上一次請求來自下載插入頁或者當前請求就是下載插入頁,那么直接返回if (ComingFromMobileInterstitial()||OnMobileInterstitial()){return;}var context = HttpContext.Current;//使用當前上下文對象創建一個MobileDetect對象var mobileDetect=new MobileDetect(context);if (mobileDetect.IsMobile()){//如果用戶拒絕下載APP,那么我們需要將他跳轉回之前訪問的頁面var url = context.Request.RawUrl;var encodeUrl = HttpUtility.UrlEncode(url);//重定向到下載插入頁,并帶上returnUrl,以防用戶需要返回到之前的頁面context.Response.Redirect("MobileInterstitial.aspx?returnUrl=" + encodeUrl);}}/// <summary>/// 檢查當前請求的前一次請求是否是來自下載插入頁/// </summary>/// <returns></returns>bool ComingFromMobileInterstitial(){var request = HttpContext.Current.Request;if (request.UrlReferrer==null){return false;}return request.UrlReferrer.AbsoluteUri.Contains("MobileInterstitial.aspx");}/// <summary>/// 判斷當前請求是不是包含插入頁文件/// </summary>/// <returns></returns>bool OnMobileInterstitial(){var request = HttpContext.Current.Request;return request.RawUrl.Contains("MobileInterstitial.aspx");}

上面只是解決了當用戶點擊拒絕下載之后用戶不會再次直接跳轉到下載插入頁的問題,用戶就不會卡在這個死循環了。但是我們還可以做得更好,假設用戶不想安裝APP,并希望在一個正常的移動端瀏覽器中查看頁面,而且,用戶點擊了拒絕下載按鈕之后,也不要每次請求都要重定向到下載插入頁。

進一步完善

當用戶點擊了“不,謝謝!”按鈕之后,我們就不要在每次頁面請求時都跳轉到下載插入頁,不要再打擾他們了。一種方式就是當用戶點擊了該按鈕之后,設置一個cookie:

protected void btnThanks_Click(object sender, EventArgs e)
{//用戶點擊拒絕下載按鈕之后,設置一個cookie,并根據自己的情況設置一個有效期,這里為了演示,設置為2分鐘var cookie=new HttpCookie("NoThanks","yes");cookie.Expires = DateTime.Now.AddMinutes(2);Response.Cookies.Add(cookie);//取到上一次請求的urlvar url = Request.QueryString.Get("returnUrl");Response.Redirect(HttpUtility.UrlDecode(url));
}

接下來,我們需要在context_BeginRequest方法中檢查是否具有特定值的Cookie,從而是否將用戶重定向到下載插入頁:

void context_BeginRequest(object sender, EventArgs e)
{//如果請求中的Cookie包含NoThanks鍵或者上一次請求來自下載插入頁或者當前請求就是下載插入頁,那么直接返回if (ExistNoThanksCookie()||ComingFromMobileInterstitial()||OnMobileInterstitial()){return;}var context = HttpContext.Current;//使用當前上下文對象創建一個MobileDetect對象var mobileDetect=new MobileDetect(context);if (mobileDetect.IsMobile()){//如果用戶拒絕下載APP,那么我們需要將他跳轉回之前訪問的頁面var url = context.Request.RawUrl;var encodeUrl = HttpUtility.UrlEncode(url);//重定向到下載插入頁,并帶上returnUrl,以防用戶需要返回到之前的頁面context.Response.Redirect("MobileInterstitial.aspx?returnUrl=" + encodeUrl);}
}bool ExistNoThanksCookie(){return HttpContext.Current.Request.Cookies.Get("NoThanks") != null;}

下面樓主將網站發布到IIS,使用Windows 10 Mobile,借助Windows 10 PC RS1版的連接功能,給大家截取動態圖演示一下效果,其他類型的手機也可以訪問網站并跳轉到對應的應用商店,但是樓主這里主要可以借助win10 PC和手機進行投影給大家演示效果。動態圖很大的,近1000幀剪輯得還剩100多幀。

577014-20160813020542640-1449368772.gif

Web應用中的HttpModule使用AOP很好地解決了橫切關注點的問題,別忘了我們這個系列的目的,是學習AOP的,而不是Web開發中的一些細節知識點,這個例子是頁面邊界切面的例子,下面我們看一個PostSharp方法邊界處理緩存的例子。

真實案例——緩存

在web開發中有一種數據庫優化的方法,比如,一個頁面可能調用了很多次數據庫,那么這些調用可以通過優化代碼和減少數據庫調用來改善性能。但是有時處理的速度不是我們能控制的,比如某些處理過程真的很復雜,需要花費很多時間來處理;有時我們需要依賴外部的處理(數據庫,web服務等等),這些我們幾乎沒有控制權。
重點來了,如果需要的數據處理的很慢,并且這些數據不經常變化,那么我們可以使用緩存來減少等待時間。Caching通常對于多用戶的系統是非常有利的,第一次的請求還是很慢的,然后緩存將第一次請求的結果存儲到可以迅速讀取數據的本地,之后其他的請求就會先去緩存檢測是否有需要的數據,如果有的話,就會直接從緩存中取出數據,從而跳過緩慢的處理過程。

緩存也可以看作是一個橫切關注點,對于想要使用緩存的每個方法,可以按照以下步驟來:

  1. 檢測緩存中是否含有值;
  2. 如果有,直接返回;
  3. 如果沒有,像以往那樣處理;
  4. 將處理的結果放到緩存中,以便下次使用。

用流程圖畫一下:

577014-20160813112409781-1968382193.png

上面的流程在代碼中都實現出來的話,可能會導致大量的樣板代碼,這就暗示我們使用AOP是個不錯的主意。下面我們看一個ASP.NET中關于Cache對象的例子,并編寫一個切面來更有效的工作。

ASP.NET Cache

不同類型的應用可以使用不同的緩存工具,比如NCache,Memcached等。但這里我們關注的是如何使用AOP處理緩存而不是各種緩存工具的使用,下面的例子會使用.Net開發者的老朋友 ASP.NET Cache。

ASP.NET代碼中的緩存就像一個可以使用的字典對象,在ASP.NET WEB Froms中,Cache繼承自Page基類,而在ASP.NET MVC中,通過繼承自Controller基類的HttpContext就可以使用緩存了。如果上面的都無法讀取緩存,可以通過HttpContext.Current.Cache獲取。

Cache對象的API很簡單,可以把它當作字典來使用,可以從Cache中獲取值,也可以往Cache中添加值。如果要獲取的值沒有存在于緩存中,就會返回null。


Cache["MyCacheKey"] = "some value";//使用MyCacheKey作為鍵存儲some value
var myValue = Cache["MyCacheKey"];//使用鍵獲取緩存
var myValue = Cache["SomeOtherKey"];//如果緩存不存在就會返回null

Cache還有很多有用的其他方法,比如AddInsert方法,這可以讓我們指定緩存的過期時間。此外,也可以使用Remove方法立即從緩存中移除一個值。

Cache 有效期

緩存值通常都會設置一個過期時間。比如,如果使用"CacheKey"存儲了一個值,并設置過期時間是2小時之后,那么2小時之后,使用"CacheKey"檢索那個值時就會返回null。
ASP.NET Cache有幾個可以使用的過期時間設置:

  1. 絕對過期時間:該值會在給定的時間過期。
  2. 滑動過期時間:該值會在上次使用之后開始計算時間,如果超過了給定的時間就會過期。
  3. 永不過期:該值會一直存在,除非應用結束掉,或者該緩存存儲了其他的東西。

關于緩存的一個案例

這次我們創建一個ASP.NET MVC項目,項目的目錄結構如下:

577014-20160814162524125-2007857430.png

上面的其他文件夾Content,Scripts,Controller,Models等等就不用多說了,不懂的話,請去學習ASP.NET MVC。下面在用一張動態圖看一下整個網站的效果:

577014-20160814163946859-1105492822.gif

這個項目是樓主從頭搭建起來的,整體布局使用的是法拉利紅作為主題色,雖然給自己的定位是全棧,但是整個頁面的布局還是花了不少時間的,看來自己還得在css和html方面深入學習一下啊。放了三個導航鏈接,Home頁隨便找了一輛自己看著還不錯的法拉利圖片,About放了兩張打賞的圖片,其實要講的東西在最后一個Value頁面。

577014-20160814164728781-448562889.gif

和之前一樣,css,html,js代碼這里就不貼出來了,感興趣的可以去看源碼,這里只放一些關于AOP的核心代碼。

Value顯示頁面

下面是點擊Value按鈕時的Action代碼,主要是放了些select中的數據和讀取緩存內容:

[HttpGet]
public ActionResult Value()
{ViewData["Cache"]= DisplayCache();//顯示緩存內容//制造商數據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");//年份數據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");//條件數據var conditions=new SelectList(new List<SelectListItem>{new SelectListItem{Text = "經濟型",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>
/// 顯示緩存內容
/// </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;
}

看到緩存里面有很多不知哪里生成的東西,就寫了個ClearAllCache()方法清除所有的緩存,但是這樣就沒辦法把自己的緩存也清除了,所以這里注釋了。這里也不貼實現了,感興趣的話請看源碼。

獲取Value的Action

選擇好各個條件之后,點擊獲取Value 按鈕就會通過ajax異步將選擇的條件提交到下面這個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();//第一種方式獲取汽車價格,不具有健壯性,故不采用//var value = carValueService.GetValue(years, makes, conditions);var value = carValueService.GetValueBetter(new CarValueArgs{Condition = conditions,Make = makes,Year = years});return Content(value.ToString("c"));
}

這個action就取到前端傳過來的條件參數,然后使用這些參數借助CarValueService服務類獲得車輛的價格。

CarValueService服務類

下面是一個汽車服務類,一般情況下,這些數據是第三方汽車廠商或代理商、分銷商等提供的,變化頻率不是很高,而且調用一個Web Service可能會很慢,因此,可以用戶緩存處理。這里我們使用Thread.Sleep(5000);來模擬一個耗時操作。這里有兩個方法,一個是GetValue,一個是GetValueBetter,上面也已經說了,后面的方法健壯性更好,因為只需要更改服務類方法的參數的屬性就夠了,而不用修改服務類方法的參數的簽名。

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);}
}

汽車的價格這里是去獲取100w到1000w之間的隨機數。方法上面都使用了緩存切面CacheAspect特性。

緩存切面CacheAspect

既然是調用第三方不頻繁變化的數據,那么就可以把請求的結果緩存起來。


[Serializable]
public class CacheAspect : OnMethodBoundaryAspect
{/// <summary>/// 進入方法前執行的邊界方法,進入服務類方法前先檢測一下緩存中是否有數據,有就直接返回緩存中的數據/// </summary>/// <param name="args"></param>public override void OnEntry(MethodExecutionArgs args){var key = GetCacheKeyBetter(args);if (HttpContext.Current.Cache[key] == null){return;//退出OnEntry方法,繼續執行服務類方法}args.ReturnValue = HttpContext.Current.Cache[key];args.FlowBehavior = FlowBehavior.Return;//這里的Return指的是跳過服務類方法}/// <summary>/// 方法成功執行后執行的邊界方法,調用第三方服務成功后緩存獲取的結果/// </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鍵,對應服務類方法有多個參數的版本/// </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鍵,升級版本,對應服務類方法只有一個對象參數/// </summary>/// <param name="args"></param>/// <returns></returns>private string GetCacheKeyBetter(MethodExecutionArgs args){//方法1:通過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);}
}

上面的代碼已經解釋地很清楚了,大家看代碼注釋就好。

這里為什么將緩存的鍵加入Json?

易讀。當看到屏幕上緩存的內容時,很清楚知道發生了什么,以及緩存了什么。
輕量。無意冒犯xml粉,但這里真不需要額外的XML頭和其他命名空間信息等標簽。
易生成。使用JsonConvert類或JavaScriptSerializer就可以輕易搞定。
其實這里選哪種方式序列化無所謂,只要能實現給緩存生成一個唯一的鍵的目的就行。

小結

這篇博文我們看了一下切面常用的類型:邊界切面。代碼中的邊界就像國家之間的分界線一樣,它給我們提供了將行為放到代碼邊界的機會。兩個常見的例子就是web頁面加載前后和方法調用前后的例子。跟方法攔截切面一樣,邊界切面提供了封裝橫切關注點的另一種方式。

PostSharp提供了編寫方法攔截切面的能力,ASP.NET通過HttpModule提供了編寫Web頁面邊界的能力,而且他們的API都提供了上下文信息(比如Http請求和方法的信息),以及控制程序流的能力(比如重定向頁面或立即從方法返回)。

這篇博客還做了好幾個示例,希望正在看博客的你能自己動手實踐一下。

轉載于:https://www.cnblogs.com/farb/p/BoundaryAspects.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/256588.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/256588.shtml
英文地址,請注明出處:http://en.pswp.cn/news/256588.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

matlab simulink筆記06 —— 利用simulink求解微分方程/simulink框圖與控制系統框圖的區別

目錄 1.利用integrator求解微分方程 1.1求解步驟 1.2例子 2.simulink框圖與控制系統框圖的區別 本人剛開始學習simulink,總是會將simulink框圖和控制系統框圖混淆,導致最后不能正確的根據simulink框圖得到相應的微

ubuntu搭建svn、git遇到的問題及解決辦法

不錯的git筆記博客&#xff1a; http://www.cnblogs.com/wanqieddy/category/406859.html http://blog.csdn.net/zxncvb/article/details/22153019 Git學習教程&#xff08;六&#xff09;Git日志 http://fsjoy.blog.51cto.com/318484/245261/ 圖解git http://my.oschina.net/x…

PHP IDE phpstorm 快捷鍵

這篇文章主要介紹了PHP IDE phpstorm 常用快捷鍵,本文分別列出了mac系統和Windows系統下的phpstorm快捷鍵,需要的朋友可以參考下 一、mac電腦phpstorm快捷鍵 command a 全選 command c 復制 command v 粘貼 command z 撤消 command k 代碼搜索 command l 輸入行號跳到某一…

Opencv SolvePnP調用實戰

1.環境說明與應用說明 VS2015opencv3.4&#xff0c;實際應用在MFC環境中&#xff01;主要是用來做定位&#xff0c;利用平面靶標給機器人的工具快換提供定位信息 2.實際調用 CV_EXPORTS_W bool solvePnP( InputArray objectPoints, InputArray imagePoints, …

matlab simulink筆記05 —— 積分模塊

1.連續積分模塊&#xff1a;integrator 例子見&#xff1a;matlab simulink筆記06 —— 利用simulink求解微分方程/simulink框圖與控制系統框圖的區別

squid在企業網中的應用

一&#xff1a;squid簡介&#xff1a; Squid是一種在Linux系統下使用的優秀的代理服務器軟件。Squid是一個緩存internet數據的一個軟件&#xff0c;它接收用戶的下載申請&#xff0c;并自動處理所下載的數據。也就是說&#xff0c;當一個用戶想要下載一個主頁時&#xff0c;它向…

win10+tensorflow faster-RCNN 訓練自己的數據集

首先&#xff0c;感謝博客上各路大佬的無私奉獻&#xff01;但是也不得不吐槽下&#xff0c;大佬些寫博客的時候能盡量寫的對小白友好一點嗎&#xff1f;期間遇到各種坑&#xff0c;說多了都是淚啊&#xff01;話不多說&#xff0c;上正題&#xff01; 環境&#xff1a;win10a…

matlab simulnk筆記07——模塊(接地模塊group、終止模塊terminal、信號合并mux與分解模塊demux)

1.接地模塊group 2.終止模塊terminal 3.信號合并mux 注意:合并僅僅指的是物理上的合并,數學上真正意義上的合并,只是將多個信號放在同一個管道上統一傳輸給顯示終端,但是每個信號之間互不影響,是相

二叉搜索樹的插入與刪除圖解

一、二叉搜索樹&#xff08;BSTree&#xff09;的概念 二叉搜索樹又被稱為二叉排序樹&#xff0c;那么它本身也是一棵二叉樹&#xff0c;那么滿足以下性質的二叉樹就是二叉搜索樹&#xff1a;1、若左子樹不為空&#xff0c;則左子樹上左右節點的值都小于根節點的值2、若它的右子…

AlienVault Ossim各版本鏡像下載地址

AlienVault Ossim各版本鏡像下載地址 OSSIM V5.0.3 ISO網盤下載地址 了解Ossim的架構、工作原理和使用方法可以參考我的新書以及http://edu.51cto.com/course/course_id-1186.html 這里提供的視頻教程。 本文轉自 李晨光 51CTO博客&#xff0c;原文鏈接&#xff1a;http://blo…

面試總結

lru算法&#xff1a;最近最少使用  1.新數據插入到鏈表頭部&#xff1b;  2.每當緩存命中&#xff08;即緩存數據被訪問&#xff09;&#xff0c;則將數據移到鏈表頭部&#xff1b;  3.當鏈表滿的時候&#xff0c;將鏈表尾部的數據丟棄。 自定義控件&#xff1a; 1.measu…

win10+anaconda安裝tensorflow和keras遇到的坑小結

win10下利用anaconda安裝tensorflow和keras的教程都大同小異&#xff08;針對CPU版本&#xff0c;我的gpu是1050TI的MAX-Q&#xff0c;不知為啥一直沒安裝成功&#xff09;&#xff0c;下面簡單說下步驟。 一 Anaconda安裝 一般來說&#xff0c;python選擇3.6的&#xff0c;目…

rman備份恢復命令之switch

一 switch 命令 1 switch命令用途 更新數據文件名為rman下鏡像拷貝時指定的數據文件名 更新數據文件名為 set newname 命令指定的名字。 2 switch 命令使用前提條件 rman 必須連接到目標數據庫 當switch tablespaces、datafiles、tempfiles時&#xff0c;這些文件必須離線 當…

服務核心 - 工具類

雖然類名稱為CWHService&#xff0c;我理解更多的是工具函數。 主要接口功能有&#xff1a; 1&#xff09;SetClipboardString設置字符串到windows剪貼板 2&#xff09;GetMachineID獲取機器標識&#xff0c;網卡地址MD5加密&#xff1b; 3&#xff09;GetMachineIDEx獲取機器標…

現代制造工程課堂筆記07——應力應變分析(考點應力莫爾圓)

目錄 選擇判斷題&#xff0c;簡單計算在莫爾圓那里出 一、單向拉伸中的應力應變 手寫筆記 選擇判斷題&#xff0c;簡單計算在莫爾圓那里出 一、單向拉伸中的應力應變 、 手寫筆記

win10+tensorflow CPU 部署CTPN環境

剛弄明白CTPN部署的時候&#xff0c;CTPN作者剛更新了簡易代碼版本&#xff0c;看介紹是把代碼優化了不需要多的配置。。。感覺好憂傷&#xff01; 源碼地址&#xff1a;https://github.com/eragonruan/text-detection-ctpn/tree/master 新版本地址&#xff1a;https://githu…

css如何實現背景透明,文字不透明?

之前做了個半透明彈層&#xff0c;但設置背景半透明時&#xff0c;子元素包含的字體及其它元素也都變成了半透明。對opacity這個屬性認識的不透徹&#xff0c;在這里做一些總結&#xff0c;方便以后使用。 背景透明&#xff0c;文字不透明的解決方法&#xff1a;為元素添加一個…

SQL Server 使用OPENROWSET訪問ORACLE遇到的各種坑總結

在SQL Server中使用OPENROWSET訪問ORACLE數據庫時&#xff0c;你可能會遇到各種坑&#xff0c;下面一一梳理一下你會遇到的一些坑。 1&#xff1a;數據庫沒有開啟"Ad Hoc Distributed Queries"選項&#xff0c;那么你就會遇到下面坑。 SELECT TOP 10 * FROM OPENROWS…

matlab——FFT傅里葉快速變換

目錄 一、自身的理解與補充 二、其他參考鏈接 一、轉載:https://blog.csdn.net/u013215903/article/details/48091359 FFT是Fast Fourier Transform(快速傅里葉變換)的簡稱,這種算法可以減少計算DFT(離散傅里葉變換,關于此更詳細的說明見后文)的時間,大大提高了運算效…