慎用System.Web.HttpContext.Current

来源:互联网 发布:为什么不造高达 知乎 编辑:程序博客网 时间:2024/06/07 01:05
每当控制流离开页面派生的Web表单上的代码的时候,HttpContext类的静态属性Current可能是有用的。 使用这个属性,我们可以获取当前请求(Request),响应(Response),会话(Session,)和应用程序对象(Application objects)以及请求更多服务。 以下面的代码为例。
private void Page_Load(object sender, System.EventArgs e){   MyClass myClass = new MyClass();   myClass.DoFoo();}class MyClass{   public void DoFoo()   {      HttpContext.Current.Response.Write("Doing Foo");   }}

  Context在同一个应用程序域中请求当前上下文的能力是强大的,但也可能被滥用。你可以从业务对象使用HttpContext.Current打破你的架构层的界限,并且很轻松地将类与ASP.NET结合,而Windows Forms和the Compact Framework PDA则无法应用于此场景。

  HttpContext.Current是如何找到上下文当前请求。 此外,它总是能找到当前请求? 例如,下面的代码的行为会是什么?

private void Page_Load(object sender, System.EventArgs e){   ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork));}public void DoWork(object state){   HttpContext context = HttpContext.Current;  context.Response.Write("Do Work");}

 

  答:上面的代码会生成一个System.NullReferenceException,因为HttpContext.Current返回null。从设计的角度来看,上面的代码至少存在两个问题,但是让我们讨论HttpContext.Current工作之前是如何工作的。

  快速查看反编译器实现Current的属性看起来像下面这样。

public static HttpContext get_Current(){     return (CallContext.GetData("HtCt") as HttpContext); }

       CallContext是类似于方法调用的线程本地存储区的专用集合对象,并提供对每个逻辑执行线程都唯一的数据槽。CallContext 为调用路径提供数据槽。CallContext.SetData 和 CallContext.GetData 可用于管理应用程序代码中的调用上下文槽。每一调用路径都有唯一的数据槽;也就是说,调用路径之间不共享状态。这些数据槽是命名过的,名称用于访问数据槽,使用该名称可以显式地释放数据槽。线程本地存储是一个概念,其中在一个应用程序域中的每个逻辑线程都有一个唯一的数据槽,以保持特定于自身的数据。 线程不共享数据,一个线程不能修改本地数据到另一个线程中。ASP.NET中,选择一个线程来执行传入的请求后,在本地的线程存储参考当前请求的上下文。现在,无论线程在哪执行(一个业务对象,数据访问对象),上下文时时存在方便检索。

 

  知道了上面我们可以声明如下:如果在处理请求时,执行移动到不同的线程(通过QueueUserWorkItem,或异步委托,作为两个例子),当前背景下HttpContext.Current将不知道如何检索,将返回null。 你可能会认为解决这个问题的一种方法是将引用传递给工作线程,就像下面的例子。


private void Page_Load(object sender, System.EventArgs e) {     WorkerClass2 worker = new WorkerClass2();     ThreadPool.QueueUserWorkItem(new WaitCallback(worker.DoWork), HttpContext.Current); } ///………class WorkerClass2 {     public void DoWork(object state)     {         HttpContext context = state as HttpContext;         Thread.Sleep(15000);         context.Response.Write("Request.Url = " + context.Request.Url);     } }

  

  然而,在我的环境中,上面的代码会抛出一个异常,此异常来源于mscoree.dll类库。上面的代码与QueueUserWorkItem例子的缺陷:两者都存在页面请求的生命周期,同时分配HttpContext对象有效生命周期。实际的开发中我们可以保持一个HttpContext参考副本,以防止垃圾回收器将HttpContext回收,ASP.NET运行时是自由的,当页面完成请求,将会自动清除一些垃圾资源。我不知道上面的代码会发生什么,在不同的情况下,代码可以在一些机器上运行,但失败的几率确实存在,并应避免这些导致失败的条件。

  有一些方法可以保证页面请求没有完成,直到工作线程完成其工作,例如,下面的代码。

private void Page_Load(object sender, System.EventArgs e){   WorkerClass worker = new WorkerClass(_resetEvent);   ThreadPool.QueueUserWorkItem(new WaitCallback(worker.DoWork),                                HttpContext.Current);   try   {      _resetEvent.WaitOne();   }   finally   {      _resetEvent.Close();   }}AutoResetEvent _resetEvent = new AutoResetEvent(false);…class WorkerClass{   public WorkerClass(AutoResetEvent resetEvent)   {        _resetEvent = resetEvent;   }   public void DoWork(object state)   {      try      {         HttpContext context = state as HttpContext;         Thread.Sleep(500);         context.Response.Write("Do work");      }      finally      {         _resetEvent.Set();      }   }   AutoResetEvent _resetEvent = null;}

  上面的代码能正常的工作在浏览器中。 然而,设计仍然存在一些疑虑。 首先,ASP.NET运行时处理多个请求的时候,它的线程使用数量有限。 我们刚刚完成相同数量的工作,但我​​们已经增加了一倍所需的线程数,产生额外的上下文切换,和现在有一个同步原语来管理。 当等待工作任务完成,在原始线程执行额外的工作,则可能是一个好处。 然而,一般来说,你更应该使用额外的线程在ASP.NET中使用一定量的保留的做法。

 

  在这篇文章中,我们已经深入了解HttpContext.Current是如何工作的,并看到了一些场景,我们需要谨慎行事。 不要滥用HttpContext.Current,每当调用在代码调用它的时候,应多检查您的设计架构。


为什么获取的System.Web.HttpContext.Current值为null,HttpContext对象为null时如何获取程序(站点)的根目录

我们先来看看HttpContext到底存储在哪里:

 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.UI; 6 using System.Web.UI.WebControls; 7  8 namespace Test_HttpContext.Current 9 {10     public partial class WebForm1 : System.Web.UI.Page11     {12         protected void Page_Load(object sender, EventArgs e)13         {14 15             HttpContext context1 = System.Web.HttpContext.Current;16 17             HttpContext context2 = System.Runtime.Remoting.Messaging.CallContext.HostContext as HttpContext;  //当前(请求)线程上下文18 19             bool isEqual = object.ReferenceEquals(context1, context2);20 21             Response.Write("context1与context2是否相同的实例:" + isEqual);22         }23     }24 }

上面的代码运行的结果是true:

从这段代码来看,HttpContext其实是保存在System.Runtime.Remoting.Messaging.CallContext.HostContext这个属性中, System.Runtime.Remoting.Messaging.CallContext.HostContext在MSDN的解释是 获取或设置与当前线程相关联的主机上下文

我们在一个ASP.NET程序中,为什么可以到处访问HttpContext.Current呢?
因为ASP.NET会为每个请求分配一个线程(也是当前线程),这个线程会执行我们的代码来生成响应结果, 即使我们的代码散落在不同的地方(类库),线程仍然会执行它们, 所以我们可以在任何地方访问System.Web.HttpContext.Current获取到与当前请求相关的HttpContext对象, 这些代码是由同一个线程来执行,所以得到的HttpContext引用也就是我们期待的那个与请求相关的对象。

当前线程是什么意思? 
我的理解是:
1. 当前线程是指与当前请求相关的线程。
2. 在ASP.NET程序中,有些线程并非总是与请求相关。

虽然在ASP.NET程序中,几乎所有的线程都应该是为响应请求而运行的,但是还有一些线程却不是为了响应请求而(产生)运行的,

例如:
1. 定时器的回调。
2. Cache的移除通知。
3. APM模式下异步完成回调。
4. 主动创建线程或者将任务交给线程池来执行。

5.异步任务Task

至于什么APM网上资料很多,这里我就不说明了

在这些情况下使用System.Web.HttpContext.Current获取HttpContext对象得到的结果都是null,因为处理他们的线程不是当前线程(为处理请求产生线程)

说的这里我们再回头看看本文开始写的(部分)代码:

1          /// <summary>2         /// 获取文件绝对路径3         /// </summary>4         /// <param name="fileName">文件名称</param>5         /// <returns></returns>6         public string Test_3(string fileName)7         {8             return System.Web.HttpContext.Current.Server.MapPath("~/Log" + fileName);9         }

如果这段代码在那5种情况下运行,都会抛空指针异常,因为System.Web.HttpContext.Current得到是null。

为什么会得到null呢?

因为运行这段代码线程不是处理当前请求的当前线程

为什么其他地方得到又不是null呢?

因为ASP.NET程序在调用您的代码前,已经将HttpContext对象设置到前面所说的System.Runtime.Remoting.Messaging.CallContext.HostContext属性中。

HttpApplication有个内部方法OnThreadEnter(),ASP.NET在调用外部代码前会调用这个方法来切换HttpContext, 例如:每当执行管线的事件处理器之前,或者同步上下文(AspNetSynchronizationContext)执行回调时。 切换线程的CallContext.HostContext属性之后,我们的代码就可以访问到HttpContext引用。 注意:HttpContext的引用其实是保存在HttpApplication对象中。

 

这种情况下该如何获取文件的绝对路径呢?

我们可以访问System.Web.HttpRuntime.AppDomainAppPath获取程序的根路径,然后再拼接文件的相对路径即可

上面的代码得到的HttpContext对象是null,再调用MapPath来获取站点根目录,就必死无疑了!

所以在此建议大家在获取程序(站点)的根目录时尽量使用System.Web.HttpRuntime.AppDomainAppPath进行获取站点的根目录

 

那么在异步调用调用时访问HttpContext对象呢?

前面我还提到在APM模式下的异步完成回调时,访问HttpContext.Current也会返回null,那么此时该怎么办呢?
1. 在类型中添加一个字段来保存HttpContext的引用(异步开始前)。
2. 将HttpContext赋值给BeginXXX方法的最后一个参数(object state)

建议优先选择第二种方法,因为可以防止以后他人维护时数据成员被意外使用。

 



 

0 0
原创粉丝点击