本篇目錄
本系列的源碼本人已托管於Coding上:點擊查看。
本系列的實驗環境:VS 2013 Update 5(建議最好使用集成了Nuget的VS版本,VS Express版也夠用),安裝PostSharp。
這篇博客覆蓋的內容包括:
- 什么是方法攔截
- 使用Castle DynamicProxy攔截方法
- 編寫數據事務切面
- 使用PostSharp攔截方法
- 編寫線程切面
第一篇博文中已經寬泛地定義了連接點和切入點,將連接點定義為代碼之間的任何點,將切入點描述為連接點的集合。這些定義不是很嚴格的,理論上,切面可以用於代碼中的任何位置:比如,可以把一個切面放到一個if語句的內部或者使用一個切面修改for循環,但是在實際應用中,99%的時間都不需要那么做。很多優秀的框架(如PostSharp和Castle DynamicProxy)使得使用預定義的連接點編寫切面很容易,並給你有限的能力描述切入點,但是你可仍然可以使用這有限的能力來處理絕大多數的AOP用例。
剩余1%的時間可以干啥?
很多低級別的工具可以讓你深入到指令級別(IL)修改或創建代碼,如Mono.Ceil,PostSharp SDK,.Net反射和Reflection.Emit。但是這個系列不是討論元編程領域的,而是介紹切面的編寫。
這篇我們會看一下方法攔截切面。這些切面可以在調用方法時,代替這些方法來運行代碼。本篇會使用兩個工具,但是方法攔截基本上是所有AOP框架最通用的功能。使用PostSharp和Castle DynamicProxy可以很容易地編寫切面,一旦使用這些框架上手了方法攔截器,那么對任何包括方法攔截的框架都可以應付自如了。
方法攔截
方法攔截切面是這么一個東西:代替被攔截的方法執行一段代碼。切面會代替方法執行,就像正常的代碼執行流程和方法之間有一個中間人一樣。為了清楚地說明這個概念,看下圖:
通過上面的兩張圖,我們就可以清楚地明白了方法攔截器的位置以及執行的次序。乍一看,方法攔截器好像另外加了一層,就像在一個事務中加了一個中間人一樣,有人就會問,為甚不直接處理呢?但是,存在即合理,也正像生活中的中間人一樣,方法攔截確實扮演了很重要的角色。
攔截器中可以放些什么呢?可以記錄即將發送的微博消息,可以驗證要發送的字符串,可以修改要發送的字符串。如果發送失敗了,可以記錄消息發送失敗,或者重新發送該消息。不需要修改Send
方法中的一行代碼就可以添加各種各樣的行為操作。
注意,不能完全取代攔截的方法。大多數情況下,切面會允許執行流繼續執行攔截的方法,我們要做的就是在方法執行之前或返回之后執行一些其他的代碼段。
PostSharp方法攔截
現在,使用上圖的例子實現代碼,我們這次使用的AOP框架是PostSharp,跟着來敲代碼,你也能學會如何編寫一個方法攔截切面。創建一個控制台程序,取名WeiBoWithPostSharp,然后對該項目添加PostSharp的引用。
PM> install-package postsharp
這里為了演示,模擬一個微博服務,然后在Main
方法中調用它的方法:
public class WeiBoClient
{
public void Send(string msg)
{
Console.WriteLine("【微博客戶端】正在發送消息:"+msg);
}
}
static void Main(string[] args)
{
var weiboService=new WeiBoClient();
weiboService.Send("hi");
Console.WriteLine();
Console.Read();
}
這只是一個簡單的程序,沒什么好說的。下面要使用PostSharp提供的API創建一個方法攔截器切面:
[Serializable]
public class MyInterceptorAspect:MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
Console.WriteLine("【攔截器:】,方法執行前攔截到的信息是:"+args.Arguments.First());//打印出攔截的方法第一個實參
args.Proceed();//Proceed()方法表示繼續執行攔截的方法
Console.WriteLine("【攔截器:】,方法已在成功{0}執行",DateTime.Now);//被攔截方法執行完成之后執行
}
}
補充一下代碼中沒有說明的3點:
- [Serializable]:當使用PostSharp時,必須要確保在切面類上使用了[Serializable]特性,因為PostSharp需要實例化並序列化切面對象,為的是能夠在編譯后反序列化這些對象使用。
- MethodInterceptionAspect:所有的方法攔截切面都必須繼承這個類。
- OnInvoke:故名思義,就是在攔截方法執行時會調用這個方法,其實准確講,被攔截的方法會在這個方法里面執行。
第一個切面就定義好了,那該怎么用呢?使用PostSharp時,直接將你的攔截方法切面以特性的方式直接標注在要攔截的方法之上即可:
[MyInterceptorAspect]
public void Send(string msg)
{
Console.WriteLine("【微博客戶端】正在發送消息:"+msg);
}
運行,看下效果:
Castle DynamicProxy方法攔截
現在,我們使用另一個AOP框架Castle DynamicProxy來編寫和上面一樣的方法攔截,這兩個工具有相似的API,就方法攔截來說,也提供了相似的功能,但是還是有很多不同的。現在,我們只需要記住的是,PostSharp是在編譯后進行工作的,而Castle DynamicProxy是在運行時工作的。
使用和上面的控制台項目相同的解決方案,另建一個控制台項目,取名WeiBoWithDynamicProxy,因為Castle DynamicProxy是Castle.Core類庫的一部分,因此需要安裝Castle.Core安裝包:
PM> Install-package castle.core
把之前那個項目的WeiboClient
類拷貝到新項目,Program的Main方法和上面項目保持一致。要使用Castle DynamicProxy創建一個切面,,需要創建一個實現了IInterceptor
的接口(該接口需要實現方法Intercept
):
public class MyInterceptorAspect:IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine("【DynamicProxy攔截器】");
}
}
現在,需要告訴DynamicProxy攔截什么代碼。使用PostSharp,可以在一個單獨的方法上應用攔截器,但是使用DynamicProxy,必須在一個完整的類的對象上使用攔截器(使用DynamicProxy的IInterceptorSeletor
也可以像PostSharp那樣定位到單個方法,但是它仍然要為整個類創建一個代理)。使用DynamicProxy,有兩個步驟:
- 創建一個
ProxyGenerator
(代理生成器)。 - 使用該
ProxyGenerator
應用攔截器。
使用常規的實例化創建一個ProxyGenerator
,然后使用它來應用攔截器,給它傳入WeiboClient
的實例,這里使用ProxyGenerator
API的CreateClassProxy
方法,因為這是演示DynamicProxy最方便的方式,后面我們會探索其他的一些用法:
static void Main(string[] args)
{
var proxyGenerator=new ProxyGenerator();//創建一個代理生成器
//下面這行代碼是為要攔截的類創建代理,第一個泛型參數就是要攔截的類,第二個參數是自定義的切面
var weiboService=proxyGenerator.CreateClassProxy<WeiBoClient>(new MyInterceptorAspect());
weiboService.Send("hello");
Console.Read();
}
好像這樣就應該沒問題了是吧,來跑一下:
按理說,應該只顯示攔截器中的內容,但這里卻輸出了微博客戶端發出的消息,所以就是說我們的攔截器沒攔截到東西。這也就是我要說的,要想成功地攔截方法,被攔截的方法必須使用virtual
關鍵字修飾,這是很重要的,沒有這個關鍵字,攔截器就不會執行,所以微博客戶端代碼修改為:
public class WeiBoClient
{
public virtual void Send(string msg)
{
Console.WriteLine("【微博客戶端】正在發送消息:"+msg);
}
}
這樣,效果就出來了:
因為攔截器里只輸出了一句話,所以我們也只看到了一句話,如果就這樣完事的話,這是不合理的,因為相當於我們把攔截的方法給吃掉了,絕大多數情況下,這樣做是沒有價值的。如果想繼續執行被攔截的方法,就可以使用和PostSharp一樣的用法:
public void Intercept(IInvocation invocation)
{
Console.WriteLine("【DynamicProxy攔截器執行開始:{0}】",DateTime.Now);
Console.WriteLine("【DynamicProxy攔截器】攔截到的方法傳入的實參是:"+invocation.Arguments.First());
invocation.Proceed();
Console.WriteLine("【DynamicProxy攔截器執行結束:{0}】",DateTime.Now);
}
運行效果如下:
Castle DynamicProxy和virtual
CreateClassProxy
返回的對象類型不是WeiboClient
,而是使用WeiboClient
作為基類動態生成的一個類型WeiboClientProxy
,就是產生了一個繼承自原來的對象的代理類(就是一個子類),因此每個要被攔截的方法必須使用virtual
,子類才可以重寫父類的方法,也就是代理類才可以正確執行,否則就會出現之前的結果,只執行了WeiboClient中的方法,而根本沒有攔截到的情況。對源碼感興趣的可以點擊閱讀。
如果你用過NHibernate,那么應該熟悉相似的需求,這不僅僅是巧合:因為NHibernate使用了Castle DynamicProxy。
如果你不喜歡這個,那么也不要抱怨,我這里只是為了演示如何攔截一個具體的類。如果我使用的是一個接口的話(IWeiboClient),那么我可以使用CreateInterfaceProxyWithTarget
的ProxyGenerator
方法代替,並且攔截的接口成員是不需要定義為virtual
的。請繼續關注此系列博客,后面會使用Castle DynamicProxy集合IoC工具StructureMap
做一些示例。
雖然這些例子都不怎么有趣,但這有助你理解方法攔截的根本。PostSharp和Castle DynamicProxy雖然在很多方面不同,但是就方法攔截的本質來說,它們都有相似的API和功能。
現在微博的例子告一段落,繼續深入一些實際的例子。后面,你會學到使用.Net中最流行的兩個AOP框架編寫攔截方法切面的基礎東西。
現實案例——數據事務
事務管理是使用數據庫工作很重要的一部分,如果涉及多個數據庫操作,經常想要這些操作全部成功或失敗,否則就會產生無效的數據或使數據不一致。
可以實現這個目標的一種方式是使用事務,事務的基本組件包括:
- 開始【begin】:標記事務開始的地方
- 執行相關的操作:例如數據庫操作,通常是2個即以上操作
- 提交【commit】:操作完成時,提交表示事務執行完畢
- 回滾【rollback】:如果操作中發生了錯誤,就不會提交了,此時會回滾,返回到最初的狀態
- 重試【retry】(可選的):不強制要求重試,但是事務回滾之后,經常可以嘗試一下重試。
事務很有用,但是它是個橫切關注點,里面可以放一些模板代碼,會對你的代碼產生噪音。因此,可以把事務方便地放到一個切面中,現在我們就來做這件事。
使用begin和commit確保數據集成
我們暫時假設所有都會成功,只需要begin和commit,而不考慮rollback。這里在原來的解決方案中,再創建一個控制台項目,取名DataTransactionCastle,很明顯,我們要使用Castle DynamicProxy,因此需要安裝它。
在演示事務之前,先來創建一些值得使用事務的代碼。比如創建一個保存發票(invoice)服務類InvoiceService
,我們會創建3個不同的保存方法:
- Save方法總是成功
- SaveRetry方法在重試之后會成功
- SaveFail總是失敗,即使在重試次數用完時也失敗
public class InvoiceService
{
public virtual void Save(Invoice invoice)
{
Console.WriteLine("已保存");
//該方法總是成功
}
private bool isRetry;
public virtual void SaveRetry(Invoice invoice)
{
if (!isRetry)
{
Console.WriteLine("第一次保存失敗");
isRetry = true;//該方法第一次總是失敗,但之后都是成功
throw new DataException();
}
Console.WriteLine("保存成功");
}
public virtual void SaveFail(Invoice invoice)
{
Console.WriteLine("保存失敗");
throw new DataException();//該方法總是拋出數據異常
}
}
public class Invoice
{
public Guid Id { get; set; }
public DateTime Date { get; set; }
public List<string> Items { get; set; }
}
注意,這些方法都使用了virtual
,在Main方法中,輸入下面的代碼:
static void Main(string[] args)
{
var srv=new InvoiceService();
var invoice=new Invoice
{
Id = Guid.NewGuid(),
Date = DateTime.Now,
Items = new List<string>() { "1","2","3"}
};
srv.Save(invoice);
//srv.SaveRetry(invoice);
//srv.SaveFail(invoice);
Console.WriteLine("執行結束!");
Console.Read();
}
最后三個Save方法要一個一個輪流執行,執行結果很簡單,這里不再演示。在實際開發中,這三種情況是在服務類的一個方法中,雖然我們很希望每次都保存成功,但總有意外存在,因此我們必須為其他場景也要做好准備。
我們可以直接將事務代碼添加到服務類中,但是想一下SRP原則,如果我們把事務代碼添加到服務類中,那么這個類就會做兩件事,所以,應該創建一個分離的攔截器以一種重用的方式來處理所有的事務相關的工作。
先來創建一個攔截器TransactionWithRetries
,之前已經假設所有事務操作都會成功了,所以代碼如下:
public class TransactionWithRetries:IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine("攔截器開始:" + DateTime.Now);
var ts = new TransactionScope();//創建一個事務范圍對象
ts.Complete();//事務完成
Console.WriteLine("攔截器結束:"+DateTime.Now);
}
}
TransactionScope
TransactionScope是System.Transactions中的類,是.NET框架中自帶的類。如果TransactionScope如果沒有調用Complete方法就被釋放(TransactionScope實現了IDisposible接口,建議使用using塊)了,那么它會認為操作執行失敗並將執行回滾。
TransactionScope是一個有用的API,它可以管理周圍事務(“周圍”意味着支持TransactionScope的數據庫可以自動管理事務),大多數主流數據庫都支持這個API,當然包括微軟自家的MSSQL。
如果你使用的數據庫或某些事務相關的系統不支持TransactionScope,那么仍然可以使用攔截器,但是必須修改代碼使用合適的支持事務的API(比如,使用BeginTransaction API可以獲得數據庫provider的IDbTransaction的實現)。
如果被攔截的方法沒有異常執行完畢了,那么就會調用TransactionScope的Complete方法,表示事務成功執行。在Main方法中使用定義的攔截切面如下:
static void Main(string[] args)
{
//var srv=new InvoiceService();
var proxyGenerator = new ProxyGenerator();
//使用被攔截的類和自定義的切面類創建動態代理
var srv = proxyGenerator.CreateClassProxy<InvoiceService>(new TransactionWithRetries());
var invoice=new Invoice
{
Id = Guid.NewGuid(),
Date = DateTime.Now,
Items = new List<string>() { "1","2","3"}
};
srv.Save(invoice);//使用這個Save方法來測試一下
//srv.SaveRetry(invoice);
//srv.SaveFail(invoice);
Console.WriteLine("Save successfully!");//輸出一句表示執行成功
Console.Read();
}
執行結果沒什么好說的:
現在理論上的場景都覆蓋到了,如果報錯了會怎樣呢?
當事務出錯時:回滾
當然,如果事務總是執行成功的話,那就不需要事務了。之所以會有事務的原因就是解決多個操作中有失敗的問題的,如果有操作失敗就回滾。
因為這里使用的.NET的TransactionScope,沒有顯式的回滾調用,最接近的等價方式是使用Dispose
方法。如果TransactionScope在Complete方法調用之前釋放,那么TransactionScope就會執行回滾。因此,需要在事務攔截器切面中添加一個Dispose
調用執行回滾。
public class TransactionWithRetries:IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine("攔截器開始:" + DateTime.Now);
var ts = new TransactionScope();//創建一個事務范圍對象
ts.Complete();//事務完成
ts.Dispose();//釋放事務范圍對象
Console.WriteLine("攔截器結束:"+DateTime.Now);
}
}
在C#中,我們可以使用一種更簡潔的語法,借助using塊,其實using語句塊結束時,會自動幫助我們調用TransactionScope的Dispose方法。
public class TransactionWithRetries:IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine("攔截器開始:" + DateTime.Now);
using (var ts = new TransactionScope())//創建一個事務范圍對象
{
invocation.Proceed();//執行被攔截的方法
ts.Complete();//事務完成
}
Console.WriteLine("攔截器結束:"+DateTime.Now);
}
}
如果被攔截的方法沒有異常執行完畢,那么就會執行ts.Complete();
,然后事務就會立即提交。如果被攔截的方法中出現了異常,那么TransactionScope就會在ts.Complete();
之前釋放(多虧了using語法和.Net的GC),觸發回滾。
現在,這個切面已經覆蓋了理想的場景Save方法,也覆蓋了最糟糕的場景SaveFail方法,下一個就是支持SaveRetry方法了。
事務操作執行失敗時:重試
前面覆蓋了總是成功和總是失敗的場景,這次要覆蓋的場景是第一次失敗時,重試一次,至於重試幾次,這個可以自己定,代碼如下:
public void Intercept(IInvocation invocation)
{
Console.WriteLine("攔截器開始:" + DateTime.Now);
var isSucceeded = false;
var retries = 3;
while (!isSucceeded)
{
using (var ts = new TransactionScope())
{
try
{
invocation.Proceed();
ts.Complete();
isSucceeded = true;
}
catch (Exception)
{
if (retries>=0)
{
Console.WriteLine("重試中...");
retries--;
}
else
{
throw;
}
}
}
}
Console.WriteLine("攔截器結束:"+DateTime.Now);
}
這個重試邏輯上一篇已經介紹過了,這里再稍微說一下。這里添加了循環進行重試,重試次數為3,如果第一次拋出了異常,那么就會執行catch塊中的代碼,那么就會輸出“重試中...”,然后重試次數遞減,再次執行和原來相同的邏輯,最后如果重試次數都用完了還沒提交事務,就只能拋出異常。
保留這個Savesrv.SaveRetry(invoice);
,注釋其他兩個Save,看一下執行結果:
當然,這里稍微優化一下,比如最大重試次數,可以移到構造函數的參數中,這樣可以方便配置,如下:
public class TransactionWithRetries:IInterceptor
{
private readonly int _maxRetries;
public TransactionWithRetries(int maxRetries)
{
_maxRetries = maxRetries;
}
public void Intercept(IInvocation invocation)
{
Console.WriteLine("攔截器開始:" + DateTime.Now);
var isSucceeded = false;
var retries = _maxRetries;
//...
配置最大重試次數的時候,只需要new的時候傳入次數值就可以了。
此外,我們還可以在提示“重試中...”的時候,具體一點,比如“重試SaveRetry方法中...”,這里提示大家一點,這個invocation參數里面有很多有趣的上下文信息,請自行查看學習,本系列不可能把每個上下文信息都介紹一遍。
比如,invocation.Method
會返回一個MethodInfo
對象,該對象來自System.Reflection命名空間,它代表被攔截的方法,因此,我們可以通過它的Name屬性拿到被攔截方法的方法名稱:
if (retries>=0)
{
Console.WriteLine("重試方法{0}中...",invocation.Method.Name);
retries--;
}
現在已經使用DynamicProxy的API完成了一個有用的攔截器切面,下面我們切換到PostSharp,再看一個現實中的攔截器切面。
現實案例——線程
當將一個程序加載到內存並開始執行時,它就是一個進程。CPU會讀取該進程的每個指令,一次讀一個。有時想要處理多個事情,比如,當等待一個緩慢的web服務時,你可能會通過UI通知用戶進度。要完成這件事,你可以使用多線程,它就像很多微處理。
雖然Web開發者沒有像桌面或者移動開發者那么多的機會使用.Net的多線程能力,但是即使對於老練的桌面開發者,多線程可能也是一個痛苦的經歷:多線程很難編寫、調試、閱讀和測試。然而,創建一個可響應的桌面體驗,編碼時多線程經常是無法避免的。
事實就是這樣,但是這里要將線程引入AOP的原因是:我們可以通過AOP做點事情,使得線程代碼編寫和閱讀稍微有點容易。
.Net線程基礎
隨着多核心編程變得越來越重要、越來越普遍,編寫多線程程序方式的數量也隨之更加。微軟和其他第三方都提供了許多值得進一步探索的線程選項。
這個例子會使用舊式的Thread
類,如果你偏愛其他編寫線程代碼的方式,那么AOP可以容易地以模塊化、封裝的方式來編寫線程代碼,而無需在多個地方橫切代碼。
假設有個耗時操作DoWork
方法,因此,需要在一個工作線程中運行它,這樣做是為了能夠釋放UI,以通知用戶當前的狀態或者允許用戶進行其它操作。要在一個線程中運行DoWork
,只需要創建一個線程對象,然后開啟線程即可:
var thead=new Thread(DoWork);
thead.Start();
雖然這行代碼看着很簡單,但是Thread類還有很多其他能力:檢查線程是否仍然活着,設置線程為后台線程,線程優先級等等。編碼時,經常需要System.Threading
的其他API,如ManualResetEvent , ThreadPool , Mutex
,很可能也需要lock
關鍵字。
多線程不是本篇的重點,就不多言了。現在,我們要做的例子只是使用了多線程中一些基本的東西。
UI線程和工作線程
創建一個WinForm項目,取名WeiboWindow,這個項目使用了線程,但沒有使用任何AOP。界面如下:
這里的需求是:點擊更新按鈕,ListBox控件中的內容會更新來自一個web服務的微博消息,當然,微博消息是模擬的:
public class WeiboService
{
public string GetMessage()
{
Thread.Sleep(3000);//模擬一個緩慢的web服務
return "消息來自" + DateTime.Now;
}
}
雙擊更新按鈕,VS會自動幫我們為更新按鈕生成一個點擊事件,我們可以在這個方法中更新ListBox的內容:
public partial class Form1 : Form//這是Form1代碼后置類,是個分部類,UI布局代碼在另一個分離的類中
{
private WeiboService _weiboService;
public Form1()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
_weiboService=new WeiboService();//當窗體加載事件觸發時,實例化一個服務類
}
private void btnUpdate_Click(object sender, EventArgs e)//更新按鈕的單擊事件
{
var msg = _weiboService.GetMessage();
listBox.Items.Add(msg);
}
}
好了,運行程序,你會發現,當點擊了更新按鈕之后的3秒內,也就是GetMessage
方法運行時,UI界面“死掉了”,點擊哪里都沒任何反應了,移動不了窗體,不能滾動ListBox的滾動條。這是因為這個進程只有一個主線程,當點擊更新按鈕后,主線程也參與了GetMessage
方法的執行,從而沒時間處理UI界面上的東西,所以給我們的表現是“界面鎖死”。
那當請求web服務時不想界面毫無響應怎么辦(也許我們會展示一個loading動畫等等)?這就需要我們創建一個工作線程來處理GetMessage
方法的執行,而原來的主線程(也就是UI線程)來處理其他操作(點擊,滾動等等)。修改代碼如下:
private void btnUpdate_Click(object sender, EventArgs e)//更新按鈕的單擊事件
{
var thread=new Thread(GetMsg);//初始化一個新的線程來處理GetMsg方法
thread.Start();//開啟線程
}
void GetMsg()
{
var msg = _weiboService.GetMessage();
listBox.Items.Add(msg);
}
現在看着好多了,執行一下(Ctrl+F5),debug模式會報錯:
現在,可以連續多次點擊更新按鈕,並且窗體可以移動,listBox的滾動條也能滾動了。如果是在debug模式下運行的話,當代碼向ListBox上添加項時,會報InvalidOperationException
錯誤,這是因為在winform應用中,UI控件是線程不安全的。就像數據庫事務一樣,如果從多個線程操作UI控件的話,會導致UI控件進入不一致的狀態。操作來自線程的(非UI線程)控件對象的方法不可取,因為在Debug模式下總是拋異常,在非Debug模式也可能會出現各種錯誤。
那么如何檢查是否運行在UI線程上呢?如果不是的話,如何讓代碼運行在UI線程上?使用繼承自Form基類的InvokeRequired
和Invoke
成員,如下:
void GetMsg()
{
var msg = _weiboService.GetMessage();
if (InvokeRequired)
{
Invoke(new Action(() => { listBox.Items.Add(msg); }));
}
else
{
listBox.Items.Add(msg);
}
}
InvokeRequired和Invoke
InvokeRequired用來詢問當前的線程是否在UI線程上。如果是true,那么當前的線程就不在UI線程上,這種情況就必須調用Invoke方法執行代碼,它可以處理winform控件。
這種模式不受限於winform。檢查當前的線程和使用UI線程的特定方式可能根據使用的應用類型而變化。WPF使用Dispatcher.CheckAccess
和Dispatcher.Invoke
。其他的UI技術,如Mono for Android,WinPhone和Silverlight可能也有變化。
代碼稍微優化一下:
void GetMsg()
{
var msg = _weiboService.GetMessage();
if (InvokeRequired)
Invoke(new Action(() => UpdateListboxItems(msg)));
else
UpdateListboxItems(msg);
}
void UpdateListboxItems(string msg)
{
listBox.Items.Add(msg);
}
現在不論是F5的debug模式還是Ctrl+F5的非debug運行模式,都不會報之前的錯了。現在這個例子很簡單,但是真實項目中涉及線程的代碼都是很凌亂的,因此,這里我們展示一下如何使得線程代碼更容易閱讀和編寫。想象一下,如果我們能在Form1類中這樣寫代碼,那么看起來簡直太漂亮了:
#region 使用了AOP版本
private void btnUpdate_Click(object sender, EventArgs e)//更新按鈕的單擊事件
{
GetMsg();
}
[WorkerThread]
void GetMsg()
{
var msg = _weiboService.GetMessage();
UpdateListboxItems(msg);
}
[UIThread]
void UpdateListboxItems(string msg)
{
listBox.Items.Add(msg);
}
#endregion
上面的代碼主要有三個變化:
- 在單擊事件中不需要在創建一個Thread對象了,只需要直接調用方法。閱讀起來更加清晰,因為沒有任何關於開啟一個新線程的噪音代碼。
GetMsg
方法有一個WorkerThread特性,聲明了它會運行在一個工作線程中,注意方法內的代碼沒有了之前的InvokeRequired和Invoke噪音代碼了,更容易閱讀,我們可以更清楚地知道它在做什么了。- UpdateListboxItems方法不變,只是加了一個UIThread特性,表明它運行在UI線程上。
上面設想的代碼更短、更具聲明式,並且沒有包含線程細節。把代碼放到一個工作線程上就像使用特性一樣簡單,而且如果想更改線程細節(比如要使用.Net 4中的Task類),只需要在一個地方處理就行了。我們可以通過AOP寫兩個小的攔截器切面就可以完成這種聲明式線程代碼了。
使用AOP的聲明式線程
要解決上面設想的場景,我們需要兩個攔截器切面,第一個是把攔截到的方法放到一個工作線程中,另一個是把攔截到的方法放到一個UI線程中。
動手時間:在剛才建的winform項目里安裝postsharp,創建一個WorkerThread
切面,它繼承自MethodInterceptionAspect
,並重寫OnInvoke方法:
[Serializable]
public class WorkerThread:MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
var thread=new Thread(args.Proceed);//將被攔截的方法傳入線程構造函數
thread.Start();
}
}
這個切面的目的是將被攔截的方法移到一個新的線程中。但是該工作線程要更新UI的話,如果我們沒有檢查InvokeRequired
,那么運行還是會出現之前的問題。所以我們還必須創建一個UIThread
切面:
[Serializable]
public class UIThread : MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
var form = args.Instance as Form;
if (form.InvokeRequired)
form.Invoke(new Action(args.Proceed));
else
args.Proceed();
}
}
這個切面的目的是檢查是否必須調用Invoke
。但是如何在獨立於Form類的切面類中使用InvokeRequired
屬性和Invoke
方法呢?幸運的是,我們可以通過args
參數來 獲得正在攔截的方法的實例對象,args.Instance
會返回一個類型為object的方法的實例對象,因此在使用InvokeRequired和Invoke
之前需要將它轉成Form類型。
MethodInterceptionArgs
類型的args參數包含了很多關於被攔截的方法的其他信息:上下文、傳入的實參等等。這和Castle DynamicProxy的IInvocation API是一樣的,建議讀者自行探索所有可用的方法和屬性。
使用了這兩個切面之后,代碼就更加可讀了,線程也更加容易使用了,此外,我們也把線程細節代碼從之前的類中解耦以及封裝到它們自己的類了。因此,如果想要切換使用Task
類的話,只需要在對應的切面中修改代碼就可以了:
public override void OnInvoke(MethodInterceptionArgs args)
{
//var thread=new Thread(args.Proceed);//將被攔截的方法傳入線程構造函數
//thread.Start();
var task=new Task(args.Proceed);
task.Start();
}
最后,再次強調一下,這篇不是講多線程的,只是為了演示多線程代碼經常會作為橫切關注點穿插在UI代碼中,使得業務代碼和橫切關注點之間糾纏交錯,而使用切面會將這些橫切關注點分離到單獨的類中。
小結
這篇覆蓋了切面最常見的類型:方法攔截切面。方法攔截切面就像調用者和被調用方法之間的中間人一樣,不需要修改被調用方法就可以添加和修改方法的行為,也提供了將橫切關注點封裝到單獨類中的能力,改善了代碼的組織和復用性。
PostSharp,Castle DynamicProxy和其他類似的工具都使得編寫切面相當簡單,它們的API都允許我們在任何時間執行被攔截的方法,這些API也提供了關於被攔截方法的上下文信息,包括該方法的信息(如方法名),以及該方法在哪個類中,傳入方法的實參等等。