問題描述
手上有一行業門戶網站,每個使用者都有一個子網站(或說模板),每個子站會有一些配置資料(如:logo,橫欄圖片或者其它一些沒想到的東西)為了編程方便以及日後擴充方便,我使用直接在.aspx檔案中定義的類來儲存這些資料,同樣為了修改方便(編譯一次站的時間已經到了讓人無法接受地步),我這裡使用的是單獨的.aspx檔案(不關聯.cs檔案),配置資料會被分配給定義的類,然後將類序列化,使用是則執行還原序列化,(基本上也就類似那個 profile的過程)
下面是序列化代碼
public static class SerializationHelper
{
public static string Serialize(object data)
{
using (MemoryStream streamMemory = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
// 2. Serialize the dataset object using the binary formatter
formatter.Serialize(streamMemory, data);
// 3. Encrypt the binary data
string binaryData = Convert.ToBase64String(streamMemory.GetBuffer());
// 4. Write the data to a file
return binaryData;
}
}
public static object Deserialize(string serialData)
{
object data = new object();
try
{
MemoryStream streamMemory;
BinaryFormatter formatter = new BinaryFormatter();
// 2. Read the binary data, and convert it to a string
string cipherData = serialData;
// 3. Decrypt the binary data
byte[] binaryData = Convert.FromBase64String(cipherData);
// 4. Rehydrate the dataset
streamMemory = new MemoryStream(binaryData);
data = formatter.Deserialize(streamMemory);
}
catch(Exception ex)
{
// data could not be deserialized
data = null;
}
return data;
}
}
這裡使用了二進位序列化,現有如下兩個檔案TemplateConfig.aspx跟Index.aspx.
TemplateConfig.aspx檔案裡面代碼如下
<script runat="server">
//配置資料保持類的定義
[Serializable]
public class Temp_EntProperty
{
private string _bar;
private string _logo;
private System.Collections.Generic.List<SubItem> size;
public System.Collections.Generic.List<SubItem> Size
{
get
{
if (size == null)
{
size = new System.Collections.Generic.List<SubItem>();
}
return size;
}
}
public string Bar
{
get { return _bar; }
set { _bar = value; }
}
public string Logo
{
get { return _logo; }
set { _logo = value; }
}
}
[Serializable]
public class SubItem
{
public SubItem(string name, int value)
{
_name = name;
_value = value;
}
private string _name;
private int _value;
public string Name
{
get { return _name; }
set { _name = value; }
}
public int Value
{
get { return _value; }
set { _value = value; }
}
}
protected void Buttion1_Click(object sender, EventArgs e)
{
Temp_EntProperty properties = new Temp_EntProperty();
config.SerializeData = SerializationHelper.Serialize(properties); //將序列化後的資料儲存到資料庫
//資料庫操作....
}
</script>
頁面標籤省略.......
Index.aspx檔案代碼如下
需要引用TemplateConfig.aspx頁
<%@ Reference Page="~/TemplateConfig.aspx" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
ASP.templateconfig_aspx.Temp_EntProperty properties=SerializationHelper.Deserialize(serialData) as ASP.templateconfig_aspx.Temp_EnterProperty ;
//serialData是資料庫中的序列化後資料.
}
</script>
頁面標籤省略.......
這個過程很多時候都能正常工作(現在知道,這是3種情況中的一種)
第一種:我先運行了TemplateConfig.aspx檔案,然後再運行Index.aspx檔案發現一切正常.
第二種:我修改了一下TemplateConfig.aspx檔案,接著再次運行Index.aspx檔案時卻出現錯誤.
單步調試發現還原序列化是正常的,但是在將還原序列化後的object類型的執行個體轉化為Temp_EntProperty時出問題了,
單步調試明明顯示Object類型的執行個體就是Temp_EntProperty,但是卻不允許轉化....
第三種:有時候還會碰到還原序列化錯誤,提示找不到程式集,而奇怪的是,TemplateConfig.aspx跟Index.aspx檔案的代碼一行沒變動過.
問題的解決
當然這個3個問題不是同發現的,事實上這些問題是在網站測試的個把月中陸續發現的,為了避免這些情況,我一度把自訂類(如 Temp_EntProperty等)改成Hashtable ,因為Hashtable不會出現以上問題,直到今天回過頭來認真的找下原因.
根據錯誤資訊大致可以判斷出是還原序列化時程式集問題(根據第三種情況),實際上以上問題的根源是,在.aspx檔案中定義的類是動態編譯的,其產生的程式集儲存在系統硬碟的.net目錄中(可以使用HttpRuntime.CodegenDir找到這個目錄),你每次對.aspx檔案的修改多會迫使asp.net運行時再次編譯這個類,產生新的程式集(以App_Web_開頭),而二進位序列化跟還原序列化是跟程式集相關的,即使用二進位序列化後的資料裡面保留了程式集資訊,而還原序列化時會根據這些資訊找到對應的程式集,完成還原序列化工作,另一方面 Asp.net頁面編譯模型中,當頁面改變後,運行時產生新的頁面程式集,會有一種機制來刪除舊的頁面程式集.
拿TemplateConfig.aspx頁面為例, 當第一次運行時,系統在臨時目錄中產生其對應 App_Web_2342usaj(注意副檔名是.dll,這裡省略,下同) 程式集,接著你修改了這個TemplateConfig.aspx頁面(打個空格什麼的儲存下就好),你第二次請求時,運行時又會產生 App_Web_asdfasd程式集,並且運行時試圖刪除App_Web_2342usaj程式集,但是不幸的是App_Web2342usaj程式集中已經被架載到AppDomain中了,Asp.net運行時無法刪除已經架載到AppDomain中的程式集,除非你重新啟動應用程式(修該下 web.config或global.asax,重新啟動IIS或應用程式集區,當然重啟電腦也可以), 在無法刪除的情況下,Asp.net運行時會在臨時目錄下添加一個名為App_Web_2342usaj.dll.delete的空檔案,這是一個標記, 在應用程式重新啟動時,AppDomain會被重新載入,而在載入之前,App_Web_2342usaj.dll檔案會被刪除,另外當你連續15次修改 TemplateConfig.aspx檔案後,應用程式也會重新啟動,不然AppDomain中就會有太多程式集而消耗記憶體(參考Asp.net2.0 進階編程),另外關於在頁面中使用Reference後,在觀察Index.aspx對應的.compiled檔案(臨時目錄下),可以發現在 index.aspx.2342s.compiled(中間數字或字元是隨機)的<filedeps>節中有對檔案TemplateConfig.aspx的引用,這個說明在修改了TemplateConfig.aspx檔案後或觸發index.aspx依賴緩衝清除,也就是index.aspx會隨之重新編譯產生新的程式集.上面這些過程你也可以自己觀察到.
好了有了上面這些知識我們現在可以解釋前面的三種情況了
第二種情況,我們修改了一下TemplateConfig.aspx檔案後接著運行Index.aspx檔案,由於Index.aspx依賴於 TemplateConfig.aspx檔案,因此TemplateConfig.aspx檔案跟Index.aspx檔案都會被重新編譯產生新的程式集,問題在於 index.aspx 代碼中
protected void Page_Load(object sender, EventArgs e)
{
ASP.templateconfig_aspx.Temp_EntProperty properties=SerializationHelper.Deserialize(serialData) as ASP.templateconfig_aspx.Temp_EnterProperty ;
//serialData是資料庫中的序列化後資料.
}
使用的serialData是原先Temp_EntProperty程式集序列化的資料,因此在還原時由於老的程式集也還在AppDomain中,故還原序列化是成功的,但是轉化卻失敗了,原因是兩個類是同名類,但是位於不同的程式集中,如果我們這個時候重新啟動下應用程式,那麼就會出現第三種情況。
第三種情況:在應用程式重新啟動後,老的程式集會被刪除,而資料庫裡儲存的恰是舊程式集中Temp_EntProperty類的序列化後的資料(其中含舊程式集名稱),應次在你進行還原序列化時無疑會碰到程式集無法找到問題.
問題找到了,那麼如何解決呢,其實也很簡單,如果你需要二進位序列化帶來的高效能跟資料量少的優勢,那麼就把需要序列化的類放到固定的程式集中 (獨立建立一個項目), 如果你想在.aspx頁面中直接定義需要序列化的類,那麼你只能使用xml序列化,因為xml序列化後的資料不保留程式集資訊.