本文主要通过分析在ASP.NET 2.0中开发ASP.NET通配符映射应用程序遇到的一些问题,来说明ASP.NET 2.0中页面编译模型的不足之处。文章中如果有不妥之处,欢迎您指出。
这里所说的ASP.NET通配符映射应用程序是指在IIS中将所有请求转发至ASP.NET 2.0运行时处理(对于IIS 5.0,就是建立.*到aspnet_isapi.dll的映射),在程序中通过实现System.Web.IhttpHandlerFactory接口来处理所有请求,实现System.Web.IhttpHandlerFactory的类就相当于一个前端控制器。典型应用就是.Text及基于.Text开发的博客园Blog软件。
在ASP.NET 1.1中,实现通配符映射应用程序大家可能比较清楚,主要是两点:
1、 实现System.Web.IhttpHandlerFactory接口,在GetHandler(HttpContext context, string requestType, string url, string path)中根据请求的url,基于一些规则,找到实际访问的页面文件,然后调用PageParser.GetCompiledPageInstance对页面进行编译并生成相应的实例处理请求。这样做的好处是:你可以使用任意的url地址,不必关心是否存在对应的页面文件,而且可以方便地控制对Web服务器上资源的访问。
2、 在web.config中加上:
< add path ="*" verb ="*" type ="Dottext.Common.UrlManager.UrlReWriteHandlerFactory,Dottext.Common" validate ="false" />
</ httpHandlers >
ASP.NET 2.0中新的页面编译模型给实现通配符映射应用程序带来意想不到的问题,下面我以博客园Blog软件为例与大家一些探讨这些问题。
在博客园Blog软件中,实现IhttpHandlerFactory接口的是Dottext.Common.UrlManager. UrlReWriteHandlerFactory,不改变在ASP.NET 1.1中实现的UrlReWriteHandlerFactory代码,直接在ASP.NET 2.0中编译并运行,当程序运行在IIS根目录下,就会在执行PageParser.GetCompiledPageInstance时出现“Object reference not set to an instance of an object”异常(运行在虚拟目录中不会出现这个问题)。这个问题是ASP.NET 2.0中的一个小Bug,之前我写的PageParser.GetCompiledPageInstance中的一个Bug及解决方法对这个问题进行了一些分析,这个问题可以通过在PageParser.GetCompiledPageInstance之前调用context.RewritePath("~/default.aspx")解决。
接着进行访问个人Blog主页的测试,比如:http://www.cnblogs.com/elivn,访问时出现错误:
在ASP.NET 2.0中,当我们第一次访问一个页面时,必不少的两个过程是: 1、页面编译 2、创建编译后的页面代码的实例。页面编译是根据所访问的url地址中的扩展名找到匹配的Build Provider对页面进行编译。这里出现的问题是由于ASP.NET 2.0运行时没找到相应的Build Provider,对http://www.cnblogs.com/elivn这样地址,由于使用了通配符映射,在ASP.NET 2.0运行时处理时,得到的扩展名是空(如果没有使用通配符映射,IIS会自动将地址改为:http://www.cnblogs.com/elivn/default.aspx)。ASP.NET 2.0在这里的设计不足之处是没有考虑这种情况,无法通过在web.config中进行相应的配置来解决这个问题。如果能提供下面的配置,这个问题就可以轻松解决:
![None.gif](http://www.cnblogs.cc2/Images/OutliningIndicators/None.gif)
![None.gif](http://www.cnblogs.cc2/Images/OutliningIndicators/None.gif)
![None.gif](http://www.cnblogs.cc2/Images/OutliningIndicators/None.gif)
对于这个问题,我的解决方法是在PageParser.GetCompiledPageInstance之前对url进行处理,在url中加上缺省文件,比如:default.aspx。如果你想使用其他的扩展名,比如:.html,需要在web.config中加上:
![None.gif](http://www.cnblogs.cc2/Images/OutliningIndicators/None.gif)
![None.gif](http://www.cnblogs.cc2/Images/OutliningIndicators/None.gif)
![None.gif](http://www.cnblogs.cc2/Images/OutliningIndicators/None.gif)
这里还有一个小bug,在上面的错误信息“Make sure is has a BuildProvider AppliesToAttribute attribute which includes the value 'Web' or 'All'.”提示需要设置AppliesToAttribute属性,实际上web.config中并不支持这样的属性,可能是正式版之前的ASP.NET 2.0支持过这个属性,后来去掉后,错误提示信息并没有修改。
解决了上面的两个问题,原以为通配符映射应用程序可以在ASP.NET 2.0中正常运行了,我在本机上测试博客园的程序,页面能正常访问。可是今天凌晨在服务器进上将网站升级到ASP.NET 2.0之后,发现ASP.NET运行时在频繁地编译页面,CPU占用一直100%,编译了一个多小时还在编译,而且编译似乎与访问量有关,访问少的站点页面还能打开,博客园主站由于访问量大,几乎无法访问。问题出在哪?于是我从PageParser.GetCompiledPageInstance的源代码寻找线索,在BuildManager.GetCacheKeyFromVirtualPath中发现可疑之处,BuildManager是根据所请求的虚拟路径创建缓存键,然后根据这个键查找或创建页面编译后的缓存对象。当对一个页面发出请求时,BuildManager会检查缓存,先从内存中检查,如果内存中没有就从缓存文件夹(Temporary ASP.NET Files)中查找,如果找到,就直接创建该类型的实例,不进行动态编译。如果没找到,就进行页面编译工作,而且查找的依据就是根据虚拟路径创建的缓存键。对于通常的页面访问方式,这样处理不会引起问题。但对于通配符映射的情况,就会带来问题。因为通配符映射时,常见情况是不同的url地址访问的却是同一个页面文件。比如博客园中每篇文章地址不同,但访问的却是同样的页面代码,如果按照目前ASP.NET 2.0中的页面编译模型,每篇文章第一次访问都要进行编译,如果博客园中的几十万篇文章被访问,就要进行几十万编译,难怪今天博客园网站升级至ASP.NET 2.0之后,服务器一直忙于编译。
经过测试情况果然这样,当然访问地址:http://www.cnblogs.com/seoxs/archive/2011/04/15/2016779.html时会在Temporary ASP.NET Files中文件夹编译生成类ASP.dudu_archive_2006_03_07_345107_html,而访问其他文章地时,也根据文章地址生成另外一个类(2006年3月12日修改:对于这个问题,通过传给PageParser.GetCompiledPageInstance一个真实的虚拟地址就能解决问题,比如在博客园程序中,对于上面的地址,改为这样的代码就行了:GetCompiledPageInstance(app+"~/default.aspx", pagepath, context))。这样编译效率实在太低了!为什么要根据虚拟路径创建缓键,设计者设计时根本没考虑到通配符映射的问题,真是糟糕的设计!如果按照ASP.NET 1.1那样根据实际访问的页面文件名创建缓存键,就可以轻松地避免这个问题。ASP.NET 2.0新的页面编译模型在这里似乎是一个败笔。更糟糕的是连让开发人员弥补这个Bug的机会都没有,System.Web.Compilation.BuildManager中没有提供一个让开发人员自己设置缓存键的方法或属性。(注:创建缓存键的方法是BuildManager. GetCacheKeyFromVirtualPath(VirtualPath virtualPath, out bool keyFromVPP))。更糟糕的是,System.Web.Compilation中的很多类都是internal,很多类的方法是灰色(Reflector用灰色显示internal static或private,颜色用的不错,让人看了就灰心),想自己调用相应方法进行页面编译几乎是不可能(用反射的方法不知能否调用,还没试过,即使能调用,也要考虑性能上的损失)。难道要自己写System.Web.Compilation中那些类去处理页面编译?我宁愿选择ASP.NET 1.1,然后等ASP.NET 2.0 SP1,SP1解决不了,等SP2......希望不要等到ASP.NET 3.0。
也许你想到了在GetHandler(HttpContext context, string requestType, string url, string path)中调用System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath来编译并创建页面的实例。这个方法我也尝试过,答案是不行,还不如PageParser.GetCompiledPageInstance,至少后者能让程序运行起来。使用BuildManager.CreateInstanceFromVirtualPath时,当访问的地址中不带扩展名时就会出现“The resource cannot be found”错误,原因是在GetVPathBuildResultInternal(VirtualPath virtualPath, bool noBuild, bool allowCrossApp, bool allowBuildInPrecompile)中调用了Util.CheckVirtualFileExists(virtualPath)对虚拟路径进行检查,检查时将虚拟路径转换为物理路径,检查当前请求的页面文件是否存在,对于通配符映射应用程序,很多地址是实际上不存在的,所以就出现“The resource cannot be found”错误。而PageParser.GetCompiledPageInstance中通过调用HostingEnvironment.AddVirtualPathToFileMapping避免了这个问题。而这个方法被
Internal保护,在代码中也无法调用。
我觉得问题的核心是ASP.NET 2.0设计者在设计时没有考虑通配符映射这样的情况。是忽略还是另有考虑,就不得而知了。但ASP.NET 1.1能正确处理这个问题,而ASP.NET 2.0却处理不了,这里很不应该的。使用通配符映射的Web应用程序用户只能望ASP.NET 2.0心叹。最近花了很大精力想把博客园的程序迁移到ASP.NET 2.0,而结果却是无法迁移到ASP.NET 2.0,令人失望! 只能寄希望微软推出相应的补丁。
还好,使用通配符映射的Web应用程序不是很多,这个问题影响不是很大。