二进制度序列化碰到的问题---- 切实认识Asp.net2.0页面编译机制
问题描述
手上有一行业门户站点,每个用户都有一个子站点(或说模板),每个子站会有一些配置数据(如: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序列化后的数据不保留程序集信息.