本篇目录
本系列的源码本人已托管于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也提供了关于被拦截方法的上下文信息,包括该方法的信息(如方法名),以及该方法在哪个类中,传入方法的实参等等。