HttpContext詳細分析

解ASP.NET的開發人員都知道它有個很是強大的對象 HttpContext,並且爲了方便,ASP.NET還爲它提供了一個靜態屬性HttpContext.Current來訪問它。安全

無處不在的HttpContext

因爲ASP.NET提供了靜態屬性HttpContext.Current,所以獲取HttpContext對象就很是方便了。
也正是由於這個緣由,因此咱們常常能見到直接訪問HttpContext.Current的代碼:異步

public class Class1
{
    public Class1()
    {
        string file = HttpContext.Current.Request.MapPath("~/App_Data/xxxxxx.xml");

        string text = System.IO.File.ReadAllText(file);

        //..........其它的操做
    }

    // 或者在一些方法中直接使用HttpContext.Current
    public void XXXXX()
    {
        string url = HttpContext.Current.Request.RawUrl;

        string username = HttpContext.Current.Session["username"].ToString();

        string value = (string)HttpContext.Current.Items["key"];
    }

    // 甚至還設計成靜態屬性
    public static string XXX
    {
        get
        {
            return (string)HttpContext.Current.Items["XXX"];
        }
    }
}
View Code

這樣的代碼,常常能在類庫項目中看到,因而可知其氾濫程度。ide

難道這些代碼真的沒有問題嗎?
有人估計會說:我寫的代碼是給ASP.NET程序使用的,又不是給控制檯程序使用,因此沒有問題。工具

真的是這樣嗎?網站

HttpContext.Current到底保存在哪裏?

的確,在一個ASP.NET程序中,幾乎任什麼時候候,咱們均可以訪問HttpContext.Current獲得一個HttpContext對象, 然而,您有沒有想過它是如何實現的呢?ui

protected void Page_Load(object sender, EventArgs e)
{
    HttpContext context1 = HttpContext.Current;

    HttpContext context2 = System.Runtime.Remoting.Messaging.CallContext.HostContext as HttpContext;

    bool isEqual = object.ReferenceEquals(context1, context2);

    Response.Write(isEqual);
}
View Code

這就是我看到的結果,不信的話您也能夠試試。url

從這段代碼來看,HttpContext實際上是保存在CallContext.HostContext這個屬性中, 若是還對HostContext感到好奇的話,能夠本身用Reflector.exe去看,再也不貼代碼了,由於有些類型和方法並非公開的。spa

來看看MSDN是如何解釋CallContext.HostContext的吧:【獲取或設置與當前線程相關聯的主機上下文。線程

這個解釋很是含糊,不過有二個關鍵詞咱們能夠記下來:【當前線程】,【關聯】。設計

我是這樣理解的:和當前線程相關聯的某個東西嗎?

咱們在一個ASP.NET程序中,爲何能夠處處訪問HttpContext.Current呢?
由於ASP.NET會爲每一個請求分配一個線程,這個線程會執行咱們的代碼來生成響應結果, 即便咱們的代碼散落在不一樣的地方(類庫),線程仍然會執行它們, 因此,咱們能夠在任何地方訪問HttpContext.Current獲取到與【當前請求】相關的HttpContext對象, 畢竟這些代碼是由同一個線程來執行的嘛,因此獲得的HttpContext引用也就是咱們期待的那個與請求相關的對象。

所以,將HttpContext.Current設計成與【當前線程】相關聯是合適的。

HttpContext並不是無處不在!

【當前線程】是個什麼意思? 

答:
1. 當前線程是指與【當前請求】相關的線程。
2. 在ASP.NET中,有些線程並不是老是與請求相關。

雖然在ASP.NET程序中,幾乎全部的線程都應該是爲響應請求而運行的,
可是,還有一些線程卻不是爲了響應請求而運行,例如:
1. 定時器的回調。
2. Cache的移除通知。
3. APM模式下異步完成回調。
4. 主動建立線程或者將任務交給線程池來執行。

在以上這些狀況中,若是線程執行到HttpContext.Current,您認爲會返回什麼?
仍是一個HttpContext的實例引用嗎?
如何是,那它與哪一個請求關聯?

顯然,在1,2二種狀況中,訪問HttpContext.Current將會返回 null 。
由於頗有可能任務在運行時根本沒有任何請求發生。
瞭解異步的人應該能很容易理解第3種狀況(就當是個結論吧)
第4種狀況就更不須要解釋了,由於確實不是當前線程。

既然是這樣,那咱們再看一下本文開頭的一段代碼:

public Class1()
{
    string file = HttpContext.Current.Request.MapPath("~/App_Data/xxxxxx.xml");

    string text = System.IO.File.ReadAllText(file);

    //..........其它的操做
}

想像一下:若是Class1是在定時器回調或者Cache的移除通知時被建立的,您認爲它還能正常運行嗎?

可能您會想:爲何我在其它任何地方又能夠訪問HttpContext.Current獲得HttpContext引用呢?
答:那是由於ASP.NET在調用你的代碼前,已經將HttpContext設置到前面所說的CallContext.HostContext屬性中。
HttpApplication有個內部方法OnThreadEnter(),ASP.NET在調用外部代碼前會調用這個方法來切換HttpContext, 例如:每當執行管線的事件處理器以前,或者同步上下文(AspNetSynchronizationContext)執行回調時。 切換線程的CallContext.HostContext屬性以後,咱們的代碼就能夠訪問到HttpContext引用。 注意:HttpContext的引用實際上是保存在HttpApplication對象中。

有時候咱們會見到【ASP.NET線程】這個詞,今天正好來講說我對這個詞的理解: 當前線程是與一個HttpContext相關的線程,因爲線程與HttpContext相關聯,也就意味着它正在處理髮送給ASP.NET的請求。 注意:這個線程仍然是線程池的線程。

如何獲取文件絕對路徑?

在定時器回調或者Cache的移除通知中,有時確實須要訪問文件,然而對於開發人員來講, 他們並不知道網站會被部署在哪一個目錄下,所以不可能寫出絕對路徑, 他們只知道相對於網站根目錄的相對路徑,爲了定位文件路徑,只能調用HttpContext.Current.Request.MapPath或者 HttpContext.Current.Server.MapPath來獲取文件的絕對路徑。 若是HttpContext.Current返回了null,那該如何如何訪問文件?

其實方法並不是MapPath一種,咱們能夠訪問HttpRuntime.AppDomainAppPath獲取網站的路徑,而後再拼接文件的相對路徑便可:

看到沒:圖片中HttpContext.Current顯示的是 null ,因此您要是再調用MapPath,就必死無疑!

在此我也奉勸你們一句:儘可能不要用MapPath,HttpRuntime.AppDomainAppPath纔是更安全的選擇。

異步調用中如何訪問HttpContext?

前面我還提到在APM模式下的異步完成回調時,訪問HttpContext.Current也會返回null,那麼此時該怎麼辦呢?

答案有二種:
1. 在類型中添加一個字段來保存HttpContext的引用(異步開始前)。
2. 將HttpContext賦值給BeginXXX方法的最後一個參數(object state)

建議優先選擇第二種方法,由於能夠防止之後他人維護時數據成員被意外使用。

安全地使用HttpContext.Current

有時咱們會寫些通用類庫給ASP.NET或者WindowsService程序來使用,例如異常記錄的工具方法。 對於ASP.NET程序來講,咱們確定但願在異常發生時,能記錄URL,表單值,Cookie等等數據,便於過後分析。 然而對於WindowsService這類程序來講,您確定沒想過要記錄Cookie吧? 那麼如何實現一個通用的功能呢?

方法其實也簡單,就是要判斷HttpContext.Current是否返回null,例以下面的示例代碼:

public static void LogException(Exception ex)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("異常發生時間:").AppendLine(DateTime.Now.ToString());
    sb.AppendLine(ex.ToString());

    // 若是是ASP.NET程序,還須要記錄URL,FORM, COOKIE之類的數據
    HttpContext context = HttpContext.Current;
    if( context != null ) {
        // 能運行到這裏,就確定是在處理ASP.NET請求,咱們能夠放心地訪問Request的全部數據
        sb.AppendLine("Url:" + context.Request.RawUrl);

        // 還有記錄什麼數據,您本身來實現吧。
    }

    System.IO.File.AppendAllText("日誌文件路徑", sb.ToString());
}

就是一個判斷,解決了全部問題,因此請忘記下面這類不安全的寫法吧:

HttpContext.Current.Request.RawUrl;
HttpContext.Current.Server.MapPath("xxxxxx");

下面的方法纔是安全的:

HttpContext context = HttpContext.Current;
if( context != null ) {
    // 在這裏訪問與請求有關的東西。
}
相關文章
相關標籤/搜索