菜鸟网站建设,建设网站怎么收费标准,搬家网站建设公司,石碣东莞网站建设返回《.Net中的AOP》系列学习总目录 本篇目录 边界切面 PostSharp方法边界方法边界 VS 方法拦截ASP.NET HttpModule边界真实案例——检查是否为移动端用户真实案例——缓存小结本系列的源码本人已托管于Coding上#xff1a;点击查看。 本系列的实验环境#xff1a;VS 2013 Up…返回《.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 nameplayerName/param/// returns/returnspublic 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 servicenew 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 方法拦截 目前方法边界切面和方法拦截切面我们都看过了那么接下来对比一下这两者有什么区别。区别肯定是存在的但这些区别是很微妙的专一的开发者可能只使用其中一种切面。这节从下面两个方面讨论一下这些区别 切面方法间的共享状态代码清晰度/意图下图是PostSharp中MethodInterceptionAspect和OnMethodBoundary切面的基本结构对比 概念上讲可以将一个边界切面转成拦截切面反之亦然只需要将左边的代码改为右边格式的代码就好了但是如果真那么简单那么这两者之间的区别是什么呢很明显答案肯定不是想象的那么简单。 切面方法间的共享状态 首先看一下共享状态。拦截切面只有一个方法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 s1new BasketballStatsService();var s2new 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/finally和Proceed()的细节我们不需要读写那些代码。 最后要说的是虽然你可能偏爱方法拦截但不要忽略了边界切面因为它可以改善代码的清晰度和简洁度。 性能和内存考虑 方法边界切面和方法拦截切面其他的重要区别是性能和内存方面这些方法的考虑取决于使用的工具的不同而不同。 在PostSharp中当使用MethodInterceptionAspect时所有的参数每次都会从栈中复制到堆中通过装箱boxing当使用OnMethodBoundaryAspect时PostSharp会检测没有使用的参数不会把这些参数装箱从而优化了代码。因此如果编写的切面没有使用方法参数那么使用OnMethodBoundaryAspect会使用更少的内存如果在多个地方都使用这个切面那么这样的做法可能是重要的注意该优化功能没有包含在PostSharp的免费版中。 方法边界不是使用AOP时唯一有用的边界类型下面我们会看一个ASP.NET HttpModule的例子这个例子对于把边界放到web页面上非常有用。 ASP.NET HttpModule边界 这里为了方便演示创建一个Asp.Net Web Form项目WebFormHttpModule,新建一个页面Demo.aspx代码如下 % Page LanguageC# AutoEventWireuptrue CodeBehindDemo.aspx.cs InheritsWebFormHttpModule.Demo %
!DOCTYPE html
html xmlnshttp://www.w3.org/1999/xhtml
head runatservertitle/title
/head
bodyform idform1 runatserverdivh1这是一个Demo页面/h1/div/form
/body
/html 在浏览器中浏览该文件时页面上会显示这是一个Demo页面这句话。对每个ASP.NET 页面的请求都会有一个很复杂的生命周期但值得注意的是该生命周期中的一部分使用了HttpModule它允许我们将代码放到ASP.NET页面的边界。要创建一个HttpModule,需要创建一个实现了IHttpModule接口的类 public class MyHttpModule:IHttpModule
{/// summary/// 释放所有的资源和数据库连接/// /summarypublic void Dispose(){throw new NotImplementedException();}/// summary/// 当HttpApplication的实例创建时运行/// /summary/// param namecontext/parampublic void Init(HttpApplication context){throw new NotImplementedException();}
} 每个模块都必须在ASP.NET的Web.config文件中配置后才可以运行。Web.config的配置可能会根据你使用的web服务器 IIS6 , IIS7 , Cassini, IIS Express等等不同而不同。要想覆盖以上服务器的所有配置可以像下面那样配置 !--II6和ASP.NET开发服务器会在这里寻找--
system.webcompilation debugtrue targetFramework4.5 /httpModules!--每个模块需要唯一的名字和类型全命名空间类名--add nameMyHttpModule typeWebFormHttpModule.MyHttpModule//httpModules
/system.websystem.webServervalidation validateIntegratedModeConfigurationfalse/modulesadd nameMyHttpModule typeWebFormHttpModule.MyHttpModule//modules
/system.webServer ASP.NET使用了多个工作进程处理即将到来的请求每个工作进程都会创建一个HttpApplication的实例每个HttpApplication实例都会创建一个HttpModule然后运行Init方法现在自定义的Init方法什么都还没写下面会使用事件句柄设置一些边界 public class MyHttpModule:IHttpModule
{/// summary/// 释放所有的资源和数据库连接/// /summarypublic void Dispose(){throw new NotImplementedException();}/// summary/// 当HttpApplication的实例创建时运行/// /summary/// param namecontext/parampublic void Init(HttpApplication context){context.BeginRequest context_BeginRequest;context.EndRequest context_EndRequest;}/// summary/// 在所有的其他页面生命周期事件结束之后运行/// /summary/// param namesender/param/// param namee/paramvoid context_EndRequest(object sender, EventArgs e){var app sender as HttpApplication;app.Response.Write(页面所有的生命周期事件结束之后);}/// summary/// 页面处理请求之前运行/// /summary/// param namesender/param/// param namee/paramvoid context_BeginRequest(object sender, EventArgs e){var app sender as HttpApplication;app.Response.Write(页面请求处理之前);}
} 虽然语法很不同但是这种感觉很像之前的方法边界切面。浏览一下页面效果如下 因为在这些边界方法中有一个HttpApplication对象因此可以有很大的灵活性和潜能完成很多事情。当检查HttpApplication的属性和事件时可以看到做许多事情而不仅是输出文本。下一节我们会使用HttpModule演示一个真实的案例检测用户是否是移动端用户。 真实案例——检查是否为移动端用户 下面再创建一个ASP.NET WebForm 项目演示一个检测用户端是否是移动端的例子。比如你通过搜索引擎搜索到一个网页然后打开网页当然进入的可能不是首页也可能是首页。如果当用户进入时该网站能根据用户的客户端类型为用户提供更好的服务那么该用户可能就会发展成为该产品的最终用户。那么问题来了怎么根据用户的客户端类型为他提供更好的服务呢请看以下流程图 项目目录见下图源码大家可以通过上面的链接拿到 详细代码就不在这里浪费地方贴出来了感兴趣的可以去下载源码学习这里只贴一部分比较核心的代码。 创建HttpModule 首先要创建自己的HttpModule然后实现IHttpModule接口默认要实现Init和Dispose方法 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大平台AndroidIOS和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/returnspublic 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 mobileDetectnew 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
/// “不谢谢”的按钮点击事件用户点击了该按钮之后需要将用户导向之前访问的url
/// /summary
/// param namesender/param
/// param namee/param
protected void btnThanks_Click(object sender, EventArgs e){//取到上一次请求的urlvar url Request.QueryString.Get(returnUrl);Response.Redirect(HttpUtility.UrlDecode(url));}
/// summary
/// 点击下载按钮之后跳转到相应的应用市场
/// /summary
/// param namesender/param
/// param namee/param
protected void btnDownload_Click(object sender, EventArgs e){var mobileDetectnew 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 mobileDetectnew 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/returnsbool ComingFromMobileInterstitial(){var request HttpContext.Current.Request;if (request.UrlReferrernull){return false;}return request.UrlReferrer.AbsoluteUri.Contains(MobileInterstitial.aspx);}/// summary/// 判断当前请求是不是包含插入页文件/// /summary/// returns/returnsbool 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 cookienew 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 mobileDetectnew 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多帧。 Web应用中的HttpModule使用AOP很好地解决了横切关注点的问题别忘了我们这个系列的目的是学习AOP的而不是Web开发中的一些细节知识点这个例子是页面边界切面的例子下面我们看一个PostSharp方法边界处理缓存的例子。 真实案例——缓存 在web开发中有一种数据库优化的方法比如一个页面可能调用了很多次数据库那么这些调用可以通过优化代码和减少数据库调用来改善性能。但是有时处理的速度不是我们能控制的比如某些处理过程真的很复杂需要花费很多时间来处理有时我们需要依赖外部的处理数据库web服务等等这些我们几乎没有控制权。 重点来了如果需要的数据处理的很慢并且这些数据不经常变化那么我们可以使用缓存来减少等待时间。Caching通常对于多用户的系统是非常有利的第一次的请求还是很慢的然后缓存将第一次请求的结果存储到可以迅速读取数据的本地之后其他的请求就会先去缓存检测是否有需要的数据如果有的话就会直接从缓存中取出数据从而跳过缓慢的处理过程。 缓存也可以看作是一个横切关注点对于想要使用缓存的每个方法可以按照以下步骤来 检测缓存中是否含有值如果有直接返回如果没有像以往那样处理将处理的结果放到缓存中以便下次使用。用流程图画一下 上面的流程在代码中都实现出来的话可能会导致大量的样板代码这就暗示我们使用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还有很多有用的其他方法比如Add 和 Insert方法这可以让我们指定缓存的过期时间。此外也可以使用Remove方法立即从缓存中移除一个值。 Cache 有效期 缓存值通常都会设置一个过期时间。比如如果使用CacheKey存储了一个值并设置过期时间是2小时之后那么2小时之后使用CacheKey检索那个值时就会返回null。 ASP.NET Cache有几个可以使用的过期时间设置 绝对过期时间该值会在给定的时间过期。滑动过期时间该值会在上次使用之后开始计算时间如果超过了给定的时间就会过期。永不过期该值会一直存在除非应用结束掉或者该缓存存储了其他的东西。 关于缓存的一个案例 这次我们创建一个ASP.NET MVC项目项目的目录结构如下 上面的其他文件夹ContentScriptsControllerModels等等就不用多说了不懂的话请去学习ASP.NET MVC。下面在用一张动态图看一下整个网站的效果 这个项目是楼主从头搭建起来的整体布局使用的是法拉利红作为主题色虽然给自己的定位是全栈但是整个页面的布局还是花了不少时间的看来自己还得在css和html方面深入学习一下啊。放了三个导航链接Home页随便找了一辆自己看着还不错的法拉利图片About放了两张打赏的图片其实要讲的东西在最后一个Value页面。 和之前一样csshtml,js代码这里就不贴出来了感兴趣的可以去看源码这里只放一些关于AOP的核心代码。 Value显示页面 下面是点击Value按钮时的Action代码,主要是放了些select中的数据和读取缓存内容 [HttpGet]
public ActionResult Value()
{ViewData[Cache] DisplayCache();//显示缓存内容//制造商数据var makes new SelectList(new ListSelectListItem{new SelectListItem{Text 法拉利,Value Ferrari,Selected true},new SelectListItem{Text 劳斯莱斯,Value Rolls-Royce},new SelectListItem{Text 迈巴赫,Value Maybach}},Value,Text);//年份数据var yearsnew SelectList(new ListSelectListItem{new SelectListItem{Text 2014年,Value 2014},new SelectListItem{Text 2015年,Value 2015},new SelectListItem{Text 2016年,Value 2016,Selected true}},Value,Text);//条件数据var conditionsnew SelectList(new ListSelectListItem{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 Liststring DisplayCache()
{var cacheListnew Liststring();//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 carValueServicenew 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(){_rannew 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 nameargs/parampublic 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 nameargs/parampublic override void OnSuccess(MethodExecutionArgs args){//var key GetCacheKey(args);var key GetCacheKeyBetter(args);HttpContext.Current.Cache[key] args.ReturnValue;}/// summary/// 获取Cache键对应服务类方法有多个参数的版本/// /summary/// param nameargs/param/// returns/returnsprivate string GetCacheKey(MethodExecutionArgs args){var contactArgs string.Join(_, args.Arguments);contactArgs args.Method.Name - contactArgs;return contactArgs;}/// summary/// 获取Cache键升级版本对应服务类方法只有一个对象参数/// /summary/// param nameargs/param/// returns/returnsprivate 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