您现在的位置是:首页 > 日记日记

深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation)

薄荷2019-09-22【日记】1人已围观

简介深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation)

Microsoft 的Visual Studio为我们在应用开发中提供的强大功能,我们是有目共睹。借助该工具,是我们的开发 显得更加高效而轻松。从Microsoft把这个IDE的名字从VS.NET 该为VS(比如原来的Visual Studio.NET 2003,现在的版本叫VS2005),可以MS对该IDE的期望和野心:MS要把它改造成一个万能的IDE。不过任何都有其两面性,对于我们广大的开发者来说,VS是我们的各种行为简单化,傻瓜化;但是在另一方面,他也会蒙蔽我们的眼睛,使我们对它背后做的事情视而不见。以我们的ASP.NET Website开发为例,编程、编译、部署都可以借助VS,有了VS一切显得如此简单,每个人都会做,但是我想很多一部分人对一个ASP.NET Website如何进行编译不会很了解。这篇文章就来谈谈背后的故事——ASP.NET是如何进行编译的。由于篇幅的问题整篇文章分两个部分:动态编译Dynamical Compilation和预编译(Precompilation)。

一、动态编译的过程

alt我们先来介绍在动态编译下的大体的执行流程:当ASP.NET收到一个基于某个page的request的时候,先判断该Page和相关的Source code是否编译过,如果没有就将其编译,如果已经编译,就是用已经Load的Assembly直接生成Page对象。在这里有下面几点需要注意:

  • 动态编译是按需编译的,ASP.NET只会编译和当前Request相关的aspx和code。
  • 动态编译是基于某个目录的,也就是说ASP.NET会把被请求的page所在的目录的所有需要编译的文件进行编译,并生成一个Assembly。
  • 除了编译生成的Assembly外,动态编译还会生成一系列的辅助文件。
  • 对相关文件的修改,会导致重新编译,但是修改对当前的Request不起作用。也就是说如果你对某个aspx进行修改,那么对于修改后抵达的Request,会导致重新编译,但是对于之前的Request使用的依然是原来编译好的Assembly。
  • 编译生成的文件被放在一个临时目录中,这个目录的地址为Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files。其具体的目录结构如右图所示。


在Temporary ASP.NET Files下的Artech.ASPNETDeployment是IIS中Virtual Directory的名称,以下两级目录的名称由Hash value构成,所以编译生成的文件就保存在c6f16246目录下。这个目录你可以通过HttpRuntime.CodegenDir获得。Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files只是一个默认的临时目录,你可以在web.config中的compilation section中设置你需要的临时目录。

 1: <compilation tempDirectory="d:\MyTempFiles" />

二、一个小例子解释动态编译是如何进行的

现在我用一个Sample来一探ASP.NET是如何进行动态编译的,下图是整个项目在VS中的结构。在这个Sample中,我建立了一个Website,在根目录下创建了两个Page:Default和Default2。 在两个子目录Part I和Part II下分别创建了两个Web page:Page1和Page2。

alt
在App_Code目录中创建了一个Utility的static class。下面是它的定义:

 1: public static class Utility
 2: {
 3: public static string ReflectAllAssmebly()
 4: {
 5: StringBuilder refllectionResult = new StringBuilder();
 6:  
 7: foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
 8: {
 9: if (!assembly.FullName.Contains("App_Web"))
 10: {
 11: continue;
 12: }
 13:  
 14: refllectionResult.Append(assembly.FullName + "<br/>");
 15: Type[] allType = assembly.GetTypes();
 16: foreach (Type typeInfo in allType)
 17: {
 18: refllectionResult.Append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + typeInfo.Name + "<br/>");
 19: }
 20: }
 21:  
 22: return refllectionResult.ToString();
 23: }
 24: }

逻辑很简单,对当前加载的所有相关的程序集(这些Assembly的Fullname以App_Web打头)进行Reflection,列出所有的类型。这个ReflectAllAssmebly将在5个Web page(Default Page和两对Page1&Page2)的Page_Load事件中被调用。

 1: protected void Page_Load(object sender, EventArgs e)
 2: {
 3: this.Response.Write(Utility.ReflectAllAssmebly());
 4: }

Default是列出所有4Page对应的Link以便我们访问它们,在我们再进行编译的情况下在IE中输入对应的URL来访问Default Page。(其他Page的Html中不具有真正的内容,是一个空的page.)

alt

通过上面的显示,我们可以看到现在有一个Assembly:App_Web_wh7-uda5。该Asssembly定一个的Type有5个,  _Default和 default_aspx分别对应Default Page,而Default2和 default2_aspxDefault2 Page的。FastObjectFactory_app_web_wh7_uda5是很重要的Type,我将会在后面对其进行深入介绍。正如我们在上面说过的,动态编译是按需编译,现在我们对Default Page进行访问,由于这次对该Website的第一次访问,所有需要的Source Code,包括aspx,code behind都要进行编译。在这个Sample中,虽然我们并没有访问Default2 page,但是我们说过,动态编译是基于目录的,由于Default Page和Default2 Page都直接置于根目录下,所以ASP.NET会把根目录下的所有文件编译到一个Assembly中。由于Page1和Page2位于子目录Part I和Part II之下,所以不会参与编译。除非我们下载对它进行Request。

alt

我们现在来访问Part I下的Page1和Page2看看会有什么结果。我们会发现,两次Request获得的输出是一样的:
通过上面的输出我们发现,当前AppDomain中被加载的Assembly多了一个:App_Web_n1mhegpg。我们可以通过定义在该Assembly中的Type的命名可以猜出该Assembly是对Part I 目录进行编译产生的。Page1和Page2的编译后的Type name变成了part_i_page1_aspx& Page1和part_i_page2_aspx& Page2。此外我们看到,该Assembly中依然有一个FastObjectFactory的Type:FastObjectFactory_app_web_n1mhegpg。在这里我需要特别指出的是,名称的后缀都是通过 Hash算法得到的。

有了上面的理论和实验结果,我想这个时候,你肯定已经想到,如果我现在对Part II的Page1和Page2进行访问,输出结果会是什么样子了。alt
如果这个时候,你查看临时目录(Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files)中该Website对应的子目录,已将会看到生成了一些列的文件。

三、Page最终被转化成什么?

我们现在来看看通过编译,一个Page到底转换成什么。我们以Part I/Page1为例。通过上面的Sampe,我们知道它最终变成了两个Type:Page1和part_i_page1_aspx。下面是Page1的定义:

 1: public class Page1 : Page, IRequiresSessionState
 2: {
 3: protected HtmlForm form1;
 4: protected void Page_Load(object sender, EventArgs e)
 5: {
 6: base.Response.Write(Utility.ReflectAllAssmebly());
 7: }
 8: protected HttpApplication ApplicationInstance
 9: {
 10: get
 11: {
 12: return this.Context.ApplicationInstance;
 13: }
 14: }
 15: protected DefaultProfile Profile
 16: {
 17: get
 18: {
 19: return (DefaultProfile)this.Context.Profile;
 20: }
 21: }
 22: }
下面是part_i_page1_aspx的定义:
 1: [CompilerGlobalScope]
 2: public class part_i_page1_aspx : Page1, IHttpHandler
 3: {
 4: // Fields
 5: private static object __fileDependencies;
 6: private static bool __initialized;
 7:  
 8: // Methods
 9: public part_i_page1_aspx();
 10: private HtmlHead __BuildControl__control2();
 11: private HtmlTitle __BuildControl__control3();
 12: private HtmlLink __BuildControl__control4();
 13: private HtmlForm __BuildControlform1();
 14: private void __BuildControlTree(part_i_page1_aspx __ctrl);
 15: protected override void FrameworkInitialize();
 16: public override int GetTypeHashCode();
 17: public override void ProcessRequest(HttpContext context);
 18: }

part_i_page1_aspx中定义了一个基于aspx的HttpHandler所需的所有操作,并且它继承了Page1。所以我们可以说Part I/Page1这个page 最终的体现为part_i_page1_aspx。进一步说,对Part I/Page1的Http Request,ASP.NET所要做的就是生成一个part_i_page1_aspx来Handle这个request就可以了。

四、FastObjectFactory

通过上面的一个简单的Sample,你已经看到每个Assembly中都会生成一个以FastObjectFactory作为前缀的Type。这是一个很重要的Type,从它的名称我们不难猜出它的作用:高效的生成对象。而生成的是什么样的对象呢?实际上就是基于每个aspx的Http request的Http handler,对于Part I/Page1就是我们上面一节分析的part_i_page1_aspx对象。我们现在通过Reflector查看我们生成的App_Web_n1mhegpg中的FastObjectFactory_app_web_n1mhegpg是如何定义的。

 1: internal class FastObjectFactory_app_web_n1mhegpg
 2: {
 3: // Methods
 4: private FastObjectFactory_app_web_n1mhegpg()
 5: {
 6: }
 7:  
 8: private static object Create_ASP_part_i_page1_aspx()
 9: {
 10: return new part_i_page1_aspx();
 11: }
 12:  
 13: private static object Create_ASP_part_i_page2_aspx()
 14: {
 15: return new part_i_page2_aspx();
 16: }
 17: }

alt

通过上面的Code,我们可以看到在FastObjectFactory中定义一系列的Create_ASP_XXX(后缀就是Page 编译生成的Type的名称)。通过这些方法,可以快速生成对一个的Page。至于为什么会叫作FastObjectFactory,我想是因为直接通过调用这个静态的方法快速地创建Page对象,从而避免使用Reflection的late binding带来的性能的影响吧。

五、Preservation Files

进行每一次编译,ASP.NET会生成一系列具有.compiled扩展名的保留文件(Preservation File)。这个文件非常重要,我们现在来深入介绍这个样一个文件。

Preservation File这个文件本质上是一个XML。它是基于每个Page的,也就是每个Page都会有一个这样的Preservation File. 无论Page对应的Directory是怎样的,与之对应的Preservation File总会保存在根目录下,所以必须有一种机制保持为处于不同Directory的Page生成的Preservation File具有不同的文件名,不管Page的名称是否一样。所以Preservation File采用下面的命名机制:

 1: [page].aspx.[folder-hash].compiled

其中[page]是Page的名称,[folder-hash]是对page所在路径的Hash Value。这样做有两个好处

  • 保证处于同一级目录的所有Preservation File具有不同的文件名。
  • 保证ASP.NET对于一个Http request可以找到Page对应的Preservation File。

下面这个Preservation File就是上面Sample中Part II/Page1.aspx对应的Preservation File,名称为default2.aspx.cdcab7d2.compiled。我们来看看XML每个Item各代表什么意思。

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <preserve resultType="3" virtualPath="/Artech.ASPNETDeployment/Part II/Page1.aspx" hash="fffffff75090c769" filehash="5f27fa390c45c52a" flags="110000" assembly="App_Web_hitlo3dt" type="ASP.part_ii_page1_aspx">
 3: <filedeps>
 4: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx" />
 5: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx.cs" />
 6: </filedeps>
 7: </preserve>
 8: 

alt

有这段XML我们可以看到,所有的内容都包含在preserve XML element中,在他中间定义了几个重要的属性.

  • virtualPath: Page的虚拟地址。

  • assembly:Assembly名称

    Microsoft 的Visual Studio为我们在应用开发中提供的强大功能,我们是有目共睹。借助该工具,是我们的开发 显得更加高效而轻松。从Microsoft把这个IDE的名字从VS.NET 该为VS(比如原来的Visual Studio.NET 2003,现在的版本叫VS2005),可以MS对该IDE的期望和野心:MS要把它改造成一个万能的IDE。不过任何都有其两面性,对于我们广大的开发者来说,VS是我们的各种行为简单化,傻瓜化;但是在另一方面,他也会蒙蔽我们的眼睛,使我们对它背后做的事情视而不见。以我们的ASP.NET Website开发为例,编程、编译、部署都可以借助VS,有了VS一切显得如此简单,每个人都会做,但是我想很多一部分人对一个ASP.NET Website如何进行编译不会很了解。这篇文章就来谈谈背后的故事——ASP.NET是如何进行编译的。由于篇幅的问题整篇文章分两个部分:动态编译Dynamical Compilation和预编译(Precompilation)。

    一、动态编译的过程

    alt我们先来介绍在动态编译下的大体的执行流程:当ASP.NET收到一个基于某个page的request的时候,先判断该Page和相关的Source code是否编译过,如果没有就将其编译,如果已经编译,就是用已经Load的Assembly直接生成Page对象。在这里有下面几点需要注意:

    • 动态编译是按需编译的,ASP.NET只会编译和当前Request相关的aspx和code。
    • 动态编译是基于某个目录的,也就是说ASP.NET会把被请求的page所在的目录的所有需要编译的文件进行编译,并生成一个Assembly。
    • 除了编译生成的Assembly外,动态编译还会生成一系列的辅助文件。
    • 对相关文件的修改,会导致重新编译,但是修改对当前的Request不起作用。也就是说如果你对某个aspx进行修改,那么对于修改后抵达的Request,会导致重新编译,但是对于之前的Request使用的依然是原来编译好的Assembly。
    • 编译生成的文件被放在一个临时目录中,这个目录的地址为Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files。其具体的目录结构如右图所示。


    在Temporary ASP.NET Files下的Artech.ASPNETDeployment是IIS中Virtual Directory的名称,以下两级目录的名称由Hash value构成,所以编译生成的文件就保存在c6f16246目录下。这个目录你可以通过HttpRuntime.CodegenDir获得。Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files只是一个默认的临时目录,你可以在web.config中的compilation section中设置你需要的临时目录。

     1: <compilation tempDirectory="d:\MyTempFiles" />

    二、一个小例子解释动态编译是如何进行的

    现在我用一个Sample来一探ASP.NET是如何进行动态编译的,下图是整个项目在VS中的结构。在这个Sample中,我建立了一个Website,在根目录下创建了两个Page:Default和Default2。 在两个子目录Part I和Part II下分别创建了两个Web page:Page1和Page2。

    alt
    在App_Code目录中创建了一个Utility的static class。下面是它的定义:

     1: public static class Utility
     2: {
     3: public static string ReflectAllAssmebly()
     4: {
     5: StringBuilder refllectionResult = new StringBuilder();
     6:  
     7: foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
     8: {
     9: if (!assembly.FullName.Contains("App_Web"))
     10: {
     11: continue;
     12: }
     13:  
     14: refllectionResult.Append(assembly.FullName + "<br/>");
     15: Type[] allType = assembly.GetTypes();
     16: foreach (Type typeInfo in allType)
     17: {
     18: refllectionResult.Append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + typeInfo.Name + "<br/>");
     19: }
     20: }
     21:  
     22: return refllectionResult.ToString();
     23: }
     24: }

    逻辑很简单,对当前加载的所有相关的程序集(这些Assembly的Fullname以App_Web打头)进行Reflection,列出所有的类型。这个ReflectAllAssmebly将在5个Web page(Default Page和两对Page1&Page2)的Page_Load事件中被调用。

     1: protected void Page_Load(object sender, EventArgs e)
     2: {
     3: this.Response.Write(Utility.ReflectAllAssmebly());
     4: }

    Default是列出所有4Page对应的Link以便我们访问它们,在我们再进行编译的情况下在IE中输入对应的URL来访问Default Page。(其他Page的Html中不具有真正的内容,是一个空的page.)

    alt

    通过上面的显示,我们可以看到现在有一个Assembly:App_Web_wh7-uda5。该Asssembly定一个的Type有5个,  _Default和 default_aspx分别对应Default Page,而Default2和 default2_aspxDefault2 Page的。FastObjectFactory_app_web_wh7_uda5是很重要的Type,我将会在后面对其进行深入介绍。正如我们在上面说过的,动态编译是按需编译,现在我们对Default Page进行访问,由于这次对该Website的第一次访问,所有需要的Source Code,包括aspx,code behind都要进行编译。在这个Sample中,虽然我们并没有访问Default2 page,但是我们说过,动态编译是基于目录的,由于Default Page和Default2 Page都直接置于根目录下,所以ASP.NET会把根目录下的所有文件编译到一个Assembly中。由于Page1和Page2位于子目录Part I和Part II之下,所以不会参与编译。除非我们下载对它进行Request。

    alt

    我们现在来访问Part I下的Page1和Page2看看会有什么结果。我们会发现,两次Request获得的输出是一样的:
    通过上面的输出我们发现,当前AppDomain中被加载的Assembly多了一个:App_Web_n1mhegpg。我们可以通过定义在该Assembly中的Type的命名可以猜出该Assembly是对Part I 目录进行编译产生的。Page1和Page2的编译后的Type name变成了part_i_page1_aspx& Page1和part_i_page2_aspx& Page2。此外我们看到,该Assembly中依然有一个FastObjectFactory的Type:FastObjectFactory_app_web_n1mhegpg。在这里我需要特别指出的是,名称的后缀都是通过 Hash算法得到的。

    有了上面的理论和实验结果,我想这个时候,你肯定已经想到,如果我现在对Part II的Page1和Page2进行访问,输出结果会是什么样子了。alt
    如果这个时候,你查看临时目录(Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files)中该Website对应的子目录,已将会看到生成了一些列的文件。

    三、Page最终被转化成什么?

    我们现在来看看通过编译,一个Page到底转换成什么。我们以Part I/Page1为例。通过上面的Sampe,我们知道它最终变成了两个Type:Page1和part_i_page1_aspx。下面是Page1的定义:

     1: public class Page1 : Page, IRequiresSessionState
     2: {
     3: protected HtmlForm form1;
     4: protected void Page_Load(object sender, EventArgs e)
     5: {
     6: base.Response.Write(Utility.ReflectAllAssmebly());
     7: }
     8: protected HttpApplication ApplicationInstance
     9: {
     10: get
     11: {
     12: return this.Context.ApplicationInstance;
     13: }
     14: }
     15: protected DefaultProfile Profile
     16: {
     17: get
     18: {
     19: return (DefaultProfile)this.Context.Profile;
     20: }
     21: }
     22: }
    下面是part_i_page1_aspx的定义:
     1: [CompilerGlobalScope]
     2: public class part_i_page1_aspx : Page1, IHttpHandler
     3: {
     4: // Fields
     5: private static object __fileDependencies;
     6: private static bool __initialized;
     7:  
     8: // Methods
     9: public part_i_page1_aspx();
     10: private HtmlHead __BuildControl__control2();
     11: private HtmlTitle __BuildControl__control3();
     12: private HtmlLink __BuildControl__control4();
     13: private HtmlForm __BuildControlform1();
     14: private void __BuildControlTree(part_i_page1_aspx __ctrl);
     15: protected override void FrameworkInitialize();
     16: public override int GetTypeHashCode();
     17: public override void ProcessRequest(HttpContext context);
     18: }

    part_i_page1_aspx中定义了一个基于aspx的HttpHandler所需的所有操作,并且它继承了Page1。所以我们可以说Part I/Page1这个page 最终的体现为part_i_page1_aspx。进一步说,对Part I/Page1的Http Request,ASP.NET所要做的就是生成一个part_i_page1_aspx来Handle这个request就可以了。

    四、FastObjectFactory

    通过上面的一个简单的Sample,你已经看到每个Assembly中都会生成一个以FastObjectFactory作为前缀的Type。这是一个很重要的Type,从它的名称我们不难猜出它的作用:高效的生成对象。而生成的是什么样的对象呢?实际上就是基于每个aspx的Http request的Http handler,对于Part I/Page1就是我们上面一节分析的part_i_page1_aspx对象。我们现在通过Reflector查看我们生成的App_Web_n1mhegpg中的FastObjectFactory_app_web_n1mhegpg是如何定义的。

     1: internal class FastObjectFactory_app_web_n1mhegpg
     2: {
     3: // Methods
     4: private FastObjectFactory_app_web_n1mhegpg()
     5: {
     6: }
     7:  
     8: private static object Create_ASP_part_i_page1_aspx()
     9: {
     10: return new part_i_page1_aspx();
     11: }
     12:  
     13: private static object Create_ASP_part_i_page2_aspx()
     14: {
     15: return new part_i_page2_aspx();
     16: }
     17: }

    alt

    通过上面的Code,我们可以看到在FastObjectFactory中定义一系列的Create_ASP_XXX(后缀就是Page 编译生成的Type的名称)。通过这些方法,可以快速生成对一个的Page。至于为什么会叫作FastObjectFactory,我想是因为直接通过调用这个静态的方法快速地创建Page对象,从而避免使用Reflection的late binding带来的性能的影响吧。

    五、Preservation Files

    进行每一次编译,ASP.NET会生成一系列具有.compiled扩展名的保留文件(Preservation File)。这个文件非常重要,我们现在来深入介绍这个样一个文件。

    Preservation File这个文件本质上是一个XML。它是基于每个Page的,也就是每个Page都会有一个这样的Preservation File. 无论Page对应的Directory是怎样的,与之对应的Preservation File总会保存在根目录下,所以必须有一种机制保持为处于不同Directory的Page生成的Preservation File具有不同的文件名,不管Page的名称是否一样。所以Preservation File采用下面的命名机制:

     1: [page].aspx.[folder-hash].compiled

    其中[page]是Page的名称,[folder-hash]是对page所在路径的Hash Value。这样做有两个好处

    • 保证处于同一级目录的所有Preservation File具有不同的文件名。
    • 保证ASP.NET对于一个Http request可以找到Page对应的Preservation File。

    下面这个Preservation File就是上面Sample中Part II/Page1.aspx对应的Preservation File,名称为default2.aspx.cdcab7d2.compiled。我们来看看XML每个Item各代表什么意思。

     1: <?xml version="1.0" encoding="utf-8"?>
     2: <preserve resultType="3" virtualPath="/Artech.ASPNETDeployment/Part II/Page1.aspx" hash="fffffff75090c769" filehash="5f27fa390c45c52a" flags="110000" assembly="App_Web_hitlo3dt" type="ASP.part_ii_page1_aspx">
     3: <filedeps>
     4: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx" />
     5: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx.cs" />
     6: </filedeps>
     7: </preserve>
     8: 

    alt

    有这段XML我们可以看到,所有的内容都包含在preserve XML element中,在他中间定义了几个重要的属性.

    • virtualPath: Page的虚拟地址。

    • assembly:Assembly名称

    • Type:Page的编译后对应的Type(Http handler)。

    • hash: 一个代表本Preservation File状态的Hash value。

    • filehash: 一个代表本Dependent File状态的Hash value。

    通过hash和filehash的缓存,ASP.NET可以判断自上一次使用以来,Preservation File和它所依赖的Dependent File是否被改动,如果真的被改动,将会重新编译。因为对于文件的任何改动都会导致该Hash value的改变。

    此外,Preservation File的<filedeps>列出了所有依赖的文件,对于Page,一般是aspx和code behind。

    六、进一步剖析整个动态编译过程

    现在我们来总结整个动态编译的过程:

    • Step1:当ASP.NET收到对于某个Page的Request,根据这个request对应的Url试着找到该page对应的Preservation File,如果没有找到,重新编译Page所在目录下的所有需要编译的文件,同时生成一些额外的文件,包括Preservation File。
    • Step2:然后解析这个Preservation File,通过hash和filehash判断文件自身或者是Dependent File是否在上一次编译之后进行过任何的修改,如果是的,则重新编译。然后重新执行Step2。
    • Step3:通过Preservation File 的assembly attribute Load对应的assembly(如果Assembly还没有被Load的话),通过type获知对应的aspx type,然后借助FastObjectFactory的对应的Create_ASP_XXX创建这个type。这个新创建的对象就是我们需要的Http Handler,在之上执行相应的操作把结果Response到客户端。

    对动态编译的讨论就到这里,在本篇文章下半部分将会讨论另一种更加有用的编译方式:《深入剖析ASP.NET的编译原理之二:预编译(Precompilation)

    Microsoft 的Visual Studio为我们在应用开发中提供的强大功能,我们是有目共睹。借助该工具,是我们的开发 显得更加高效而轻松。从Microsoft把这个IDE的名字从VS.NET 该为VS(比如原来的Visual Studio.NET 2003,现在的版本叫VS2005),可以MS对该IDE的期望和野心:MS要把它改造成一个万能的IDE。不过任何都有其两面性,对于我们广大的开发者来说,VS是我们的各种行为简单化,傻瓜化;但是在另一方面,他也会蒙蔽我们的眼睛,使我们对它背后做的事情视而不见。以我们的ASP.NET Website开发为例,编程、编译、部署都可以借助VS,有了VS一切显得如此简单,每个人都会做,但是我想很多一部分人对一个ASP.NET Website如何进行编译不会很了解。这篇文章就来谈谈背后的故事——ASP.NET是如何进行编译的。由于篇幅的问题整篇文章分两个部分:动态编译Dynamical Compilation和预编译(Precompilation)。

    一、动态编译的过程

    alt我们先来介绍在动态编译下的大体的执行流程:当ASP.NET收到一个基于某个page的request的时候,先判断该Page和相关的Source code是否编译过,如果没有就将其编译,如果已经编译,就是用已经Load的Assembly直接生成Page对象。在这里有下面几点需要注意:

    • 动态编译是按需编译的,ASP.NET只会编译和当前Request相关的aspx和code。
    • 动态编译是基于某个目录的,也就是说ASP.NET会把被请求的page所在的目录的所有需要编译的文件进行编译,并生成一个Assembly。
    • 除了编译生成的Assembly外,动态编译还会生成一系列的辅助文件。
    • 对相关文件的修改,会导致重新编译,但是修改对当前的Request不起作用。也就是说如果你对某个aspx进行修改,那么对于修改后抵达的Request,会导致重新编译,但是对于之前的Request使用的依然是原来编译好的Assembly。
    • 编译生成的文件被放在一个临时目录中,这个目录的地址为Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files。其具体的目录结构如右图所示。


    在Temporary ASP.NET Files下的Artech.ASPNETDeployment是IIS中Virtual Directory的名称,以下两级目录的名称由Hash value构成,所以编译生成的文件就保存在c6f16246目录下。这个目录你可以通过HttpRuntime.CodegenDir获得。Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files只是一个默认的临时目录,你可以在web.config中的compilation section中设置你需要的临时目录。

     1: <compilation tempDirectory="d:\MyTempFiles" />

    二、一个小例子解释动态编译是如何进行的

    现在我用一个Sample来一探ASP.NET是如何进行动态编译的,下图是整个项目在VS中的结构。在这个Sample中,我建立了一个Website,在根目录下创建了两个Page:Default和Default2。 在两个子目录Part I和Part II下分别创建了两个Web page:Page1和Page2。

    alt
    在App_Code目录中创建了一个Utility的static class。下面是它的定义:

     1: public static class Utility
     2: {
     3: public static string ReflectAllAssmebly()
     4: {
     5: StringBuilder refllectionResult = new StringBuilder();
     6:  
     7: foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
     8: {
     9: if (!assembly.FullName.Contains("App_Web"))
     10: {
     11: continue;
     12: }
     13:  
     14: refllectionResult.Append(assembly.FullName + "<br/>");
     15: Type[] allType = assembly.GetTypes();
     16: foreach (Type typeInfo in allType)
     17: {
     18: refllectionResult.Append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + typeInfo.Name + "<br/>");
     19: }
     20: }
     21:  
     22: return refllectionResult.ToString();
     23: }
     24: }

    逻辑很简单,对当前加载的所有相关的程序集(这些Assembly的Fullname以App_Web打头)进行Reflection,列出所有的类型。这个ReflectAllAssmebly将在5个Web page(Default Page和两对Page1&Page2)的Page_Load事件中被调用。

     1: protected void Page_Load(object sender, EventArgs e)
     2: {
     3: this.Response.Write(Utility.ReflectAllAssmebly());
     4: }

    Default是列出所有4Page对应的Link以便我们访问它们,在我们再进行编译的情况下在IE中输入对应的URL来访问Default Page。(其他Page的Html中不具有真正的内容,是一个空的page.)

    alt

    通过上面的显示,我们可以看到现在有一个Assembly:App_Web_wh7-uda5。该Asssembly定一个的Type有5个,  _Default和 default_aspx分别对应Default Page,而Default2和 default2_aspxDefault2 Page的。FastObjectFactory_app_web_wh7_uda5是很重要的Type,我将会在后面对其进行深入介绍。正如我们在上面说过的,动态编译是按需编译,现在我们对Default Page进行访问,由于这次对该Website的第一次访问,所有需要的Source Code,包括aspx,code behind都要进行编译。在这个Sample中,虽然我们并没有访问Default2 page,但是我们说过,动态编译是基于目录的,由于Default Page和Default2 Page都直接置于根目录下,所以ASP.NET会把根目录下的所有文件编译到一个Assembly中。由于Page1和Page2位于子目录Part I和Part II之下,所以不会参与编译。除非我们下载对它进行Request。

    alt

    我们现在来访问Part I下的Page1和Page2看看会有什么结果。我们会发现,两次Request获得的输出是一样的:
    通过上面的输出我们发现,当前AppDomain中被加载的Assembly多了一个:App_Web_n1mhegpg。我们可以通过定义在该Assembly中的Type的命名可以猜出该Assembly是对Part I 目录进行编译产生的。Page1和Page2的编译后的Type name变成了part_i_page1_aspx& Page1和part_i_page2_aspx& Page2。此外我们看到,该Assembly中依然有一个FastObjectFactory的Type:FastObjectFactory_app_web_n1mhegpg。在这里我需要特别指出的是,名称的后缀都是通过 Hash算法得到的。

    有了上面的理论和实验结果,我想这个时候,你肯定已经想到,如果我现在对Part II的Page1和Page2进行访问,输出结果会是什么样子了。alt
    如果这个时候,你查看临时目录(Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files)中该Website对应的子目录,已将会看到生成了一些列的文件。

    三、Page最终被转化成什么?

    我们现在来看看通过编译,一个Page到底转换成什么。我们以Part I/Page1为例。通过上面的Sampe,我们知道它最终变成了两个Type:Page1和part_i_page1_aspx。下面是Page1的定义:

     1: public class Page1 : Page, IRequiresSessionState
     2: {
     3: protected HtmlForm form1;
     4: protected void Page_Load(object sender, EventArgs e)
     5: {
     6: base.Response.Write(Utility.ReflectAllAssmebly());
     7: }
     8: protected HttpApplication ApplicationInstance
     9: {
     10: get
     11: {
     12: return this.Context.ApplicationInstance;
     13: }
     14: }
     15: protected DefaultProfile Profile
     16: {
     17: get
     18: {
     19: return (DefaultProfile)this.Context.Profile;
     20: }
     21: }
     22: }
    下面是part_i_page1_aspx的定义:
     1: [CompilerGlobalScope]
     2: public class part_i_page1_aspx : Page1, IHttpHandler
     3: {
     4: // Fields
     5: private static object __fileDependencies;
     6: private static bool __initialized;
     7:  
     8: // Methods
     9: public part_i_page1_aspx();
     10: private HtmlHead __BuildControl__control2();
     11: private HtmlTitle __BuildControl__control3();
     12: private HtmlLink __BuildControl__control4();
     13: private HtmlForm __BuildControlform1();
     14: private void __BuildControlTree(part_i_page1_aspx __ctrl);
     15: protected override void FrameworkInitialize();
     16: public override int GetTypeHashCode();
     17: public override void ProcessRequest(HttpContext context);
     18: }

    part_i_page1_aspx中定义了一个基于aspx的HttpHandler所需的所有操作,并且它继承了Page1。所以我们可以说Part I/Page1这个page 最终的体现为part_i_page1_aspx。进一步说,对Part I/Page1的Http Request,ASP.NET所要做的就是生成一个part_i_page1_aspx来Handle这个request就可以了。

    四、FastObjectFactory

    通过上面的一个简单的Sample,你已经看到每个Assembly中都会生成一个以FastObjectFactory作为前缀的Type。这是一个很重要的Type,从它的名称我们不难猜出它的作用:高效的生成对象。而生成的是什么样的对象呢?实际上就是基于每个aspx的Http request的Http handler,对于Part I/Page1就是我们上面一节分析的part_i_page1_aspx对象。我们现在通过Reflector查看我们生成的App_Web_n1mhegpg中的FastObjectFactory_app_web_n1mhegpg是如何定义的。

     1: internal class FastObjectFactory_app_web_n1mhegpg
     2: {
     3: // Methods
     4: private FastObjectFactory_app_web_n1mhegpg()
     5: {
     6: }
     7:  
     8: private static object Create_ASP_part_i_page1_aspx()
     9: {
     10: return new part_i_page1_aspx();
     11: }
     12:  
     13: private static object Create_ASP_part_i_page2_aspx()
     14: {
     15: return new part_i_page2_aspx();
     16: }
     17: }

    alt

    通过上面的Code,我们可以看到在FastObjectFactory中定义一系列的Create_ASP_XXX(后缀就是Page 编译生成的Type的名称)。通过这些方法,可以快速生成对一个的Page。至于为什么会叫作FastObjectFactory,我想是因为直接通过调用这个静态的方法快速地创建Page对象,从而避免使用Reflection的late binding带来的性能的影响吧。

    五、Preservation Files

    进行每一次编译,ASP.NET会生成一系列具有.compiled扩展名的保留文件(Preservation File)。这个文件非常重要,我们现在来深入介绍这个样一个文件。

    Preservation File这个文件本质上是一个XML。它是基于每个Page的,也就是每个Page都会有一个这样的Preservation File. 无论Page对应的Directory是怎样的,与之对应的Preservation File总会保存在根目录下,所以必须有一种机制保持为处于不同Directory的Page生成的Preservation File具有不同的文件名,不管Page的名称是否一样。所以Preservation File采用下面的命名机制:

     1: [page].aspx.[folder-hash].compiled

    其中[page]是Page的名称,[folder-hash]是对page所在路径的Hash Value。这样做有两个好处

    • 保证处于同一级目录的所有Preservation File具有不同的文件名。
    • 保证ASP.NET对于一个Http request可以找到Page对应的Preservation File。

    下面这个Preservation File就是上面Sample中Part II/Page1.aspx对应的Preservation File,名称为default2.aspx.cdcab7d2.compiled。我们来看看XML每个Item各代表什么意思。

     1: <?xml version="1.0" encoding="utf-8"?>
     2: <preserve resultType="3" virtualPath="/Artech.ASPNETDeployment/Part II/Page1.aspx" hash="fffffff75090c769" filehash="5f27fa390c45c52a" flags="110000" assembly="App_Web_hitlo3dt" type="ASP.part_ii_page1_aspx">
     3: <filedeps>
     4: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx" />
     5: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx.cs" />
     6: </filedeps>
     7: </preserve>
     8: 

    alt

    有这段XML我们可以看到,所有的内容都包含在preserve XML element中,在他中间定义了几个重要的属性.

    • virtualPath: Page的虚拟地址。

    • assembly:Assembly名称

      Microsoft 的Visual Studio为我们在应用开发中提供的强大功能,我们是有目共睹。借助该工具,是我们的开发 显得更加高效而轻松。从Microsoft把这个IDE的名字从VS.NET 该为VS(比如原来的Visual Studio.NET 2003,现在的版本叫VS2005),可以MS对该IDE的期望和野心:MS要把它改造成一个万能的IDE。不过任何都有其两面性,对于我们广大的开发者来说,VS是我们的各种行为简单化,傻瓜化;但是在另一方面,他也会蒙蔽我们的眼睛,使我们对它背后做的事情视而不见。以我们的ASP.NET Website开发为例,编程、编译、部署都可以借助VS,有了VS一切显得如此简单,每个人都会做,但是我想很多一部分人对一个ASP.NET Website如何进行编译不会很了解。这篇文章就来谈谈背后的故事——ASP.NET是如何进行编译的。由于篇幅的问题整篇文章分两个部分:动态编译Dynamical Compilation和预编译(Precompilation)。

      一、动态编译的过程

      alt我们先来介绍在动态编译下的大体的执行流程:当ASP.NET收到一个基于某个page的request的时候,先判断该Page和相关的Source code是否编译过,如果没有就将其编译,如果已经编译,就是用已经Load的Assembly直接生成Page对象。在这里有下面几点需要注意:

      • 动态编译是按需编译的,ASP.NET只会编译和当前Request相关的aspx和code。
      • 动态编译是基于某个目录的,也就是说ASP.NET会把被请求的page所在的目录的所有需要编译的文件进行编译,并生成一个Assembly。
      • 除了编译生成的Assembly外,动态编译还会生成一系列的辅助文件。
      • 对相关文件的修改,会导致重新编译,但是修改对当前的Request不起作用。也就是说如果你对某个aspx进行修改,那么对于修改后抵达的Request,会导致重新编译,但是对于之前的Request使用的依然是原来编译好的Assembly。
      • 编译生成的文件被放在一个临时目录中,这个目录的地址为Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files。其具体的目录结构如右图所示。


      在Temporary ASP.NET Files下的Artech.ASPNETDeployment是IIS中Virtual Directory的名称,以下两级目录的名称由Hash value构成,所以编译生成的文件就保存在c6f16246目录下。这个目录你可以通过HttpRuntime.CodegenDir获得。Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files只是一个默认的临时目录,你可以在web.config中的compilation section中设置你需要的临时目录。

       1: <compilation tempDirectory="d:\MyTempFiles" />

      二、一个小例子解释动态编译是如何进行的

      现在我用一个Sample来一探ASP.NET是如何进行动态编译的,下图是整个项目在VS中的结构。在这个Sample中,我建立了一个Website,在根目录下创建了两个Page:Default和Default2。 在两个子目录Part I和Part II下分别创建了两个Web page:Page1和Page2。

      alt
      在App_Code目录中创建了一个Utility的static class。下面是它的定义:

       1: public static class Utility
       2: {
       3: public static string ReflectAllAssmebly()
       4: {
       5: StringBuilder refllectionResult = new StringBuilder();
       6:  
       7: foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
       8: {
       9: if (!assembly.FullName.Contains("App_Web"))
       10: {
       11: continue;
       12: }
       13:  
       14: refllectionResult.Append(assembly.FullName + "<br/>");
       15: Type[] allType = assembly.GetTypes();
       16: foreach (Type typeInfo in allType)
       17: {
       18: refllectionResult.Append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + typeInfo.Name + "<br/>");
       19: }
       20: }
       21:  
       22: return refllectionResult.ToString();
       23: }
       24: }

      逻辑很简单,对当前加载的所有相关的程序集(这些Assembly的Fullname以App_Web打头)进行Reflection,列出所有的类型。这个ReflectAllAssmebly将在5个Web page(Default Page和两对Page1&Page2)的Page_Load事件中被调用。

       1: protected void Page_Load(object sender, EventArgs e)
       2: {
       3: this.Response.Write(Utility.ReflectAllAssmebly());
       4: }

      Default是列出所有4Page对应的Link以便我们访问它们,在我们再进行编译的情况下在IE中输入对应的URL来访问Default Page。(其他Page的Html中不具有真正的内容,是一个空的page.)

      alt

      通过上面的显示,我们可以看到现在有一个Assembly:App_Web_wh7-uda5。该Asssembly定一个的Type有5个,  _Default和 default_aspx分别对应Default Page,而Default2和 default2_aspxDefault2 Page的。FastObjectFactory_app_web_wh7_uda5是很重要的Type,我将会在后面对其进行深入介绍。正如我们在上面说过的,动态编译是按需编译,现在我们对Default Page进行访问,由于这次对该Website的第一次访问,所有需要的Source Code,包括aspx,code behind都要进行编译。在这个Sample中,虽然我们并没有访问Default2 page,但是我们说过,动态编译是基于目录的,由于Default Page和Default2 Page都直接置于根目录下,所以ASP.NET会把根目录下的所有文件编译到一个Assembly中。由于Page1和Page2位于子目录Part I和Part II之下,所以不会参与编译。除非我们下载对它进行Request。

      alt

      我们现在来访问Part I下的Page1和Page2看看会有什么结果。我们会发现,两次Request获得的输出是一样的:
      通过上面的输出我们发现,当前AppDomain中被加载的Assembly多了一个:App_Web_n1mhegpg。我们可以通过定义在该Assembly中的Type的命名可以猜出该Assembly是对Part I 目录进行编译产生的。Page1和Page2的编译后的Type name变成了part_i_page1_aspx& Page1和part_i_page2_aspx& Page2。此外我们看到,该Assembly中依然有一个FastObjectFactory的Type:FastObjectFactory_app_web_n1mhegpg。在这里我需要特别指出的是,名称的后缀都是通过 Hash算法得到的。

      有了上面的理论和实验结果,我想这个时候,你肯定已经想到,如果我现在对Part II的Page1和Page2进行访问,输出结果会是什么样子了。alt
      如果这个时候,你查看临时目录(Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files)中该Website对应的子目录,已将会看到生成了一些列的文件。

      三、Page最终被转化成什么?

      我们现在来看看通过编译,一个Page到底转换成什么。我们以Part I/Page1为例。通过上面的Sampe,我们知道它最终变成了两个Type:Page1和part_i_page1_aspx。下面是Page1的定义:

       1: public class Page1 : Page, IRequiresSessionState
       2: {
       3: protected HtmlForm form1;
       4: protected void Page_Load(object sender, EventArgs e)
       5: {
       6: base.Response.Write(Utility.ReflectAllAssmebly());
       7: }
       8: protected HttpApplication ApplicationInstance
       9: {
       10: get
       11: {
       12: return this.Context.ApplicationInstance;
       13: }
       14: }
       15: protected DefaultProfile Profile
       16: {
       17: get
       18: {
       19: return (DefaultProfile)this.Context.Profile;
       20: }
       21: }
       22: }
      下面是part_i_page1_aspx的定义:
       1: [CompilerGlobalScope]
       2: public class part_i_page1_aspx : Page1, IHttpHandler
       3: {
       4: // Fields
       5: private static object __fileDependencies;
       6: private static bool __initialized;
       7:  
       8: // Methods
       9: public part_i_page1_aspx();
       10: private HtmlHead __BuildControl__control2();
       11: private HtmlTitle __BuildControl__control3();
       12: private HtmlLink __BuildControl__control4();
       13: private HtmlForm __BuildControlform1();
       14: private void __BuildControlTree(part_i_page1_aspx __ctrl);
       15: protected override void FrameworkInitialize();
       16: public override int GetTypeHashCode();
       17: public override void ProcessRequest(HttpContext context);
       18: }

      part_i_page1_aspx中定义了一个基于aspx的HttpHandler所需的所有操作,并且它继承了Page1。所以我们可以说Part I/Page1这个page 最终的体现为part_i_page1_aspx。进一步说,对Part I/Page1的Http Request,ASP.NET所要做的就是生成一个part_i_page1_aspx来Handle这个request就可以了。

      四、FastObjectFactory

      通过上面的一个简单的Sample,你已经看到每个Assembly中都会生成一个以FastObjectFactory作为前缀的Type。这是一个很重要的Type,从它的名称我们不难猜出它的作用:高效的生成对象。而生成的是什么样的对象呢?实际上就是基于每个aspx的Http request的Http handler,对于Part I/Page1就是我们上面一节分析的part_i_page1_aspx对象。我们现在通过Reflector查看我们生成的App_Web_n1mhegpg中的FastObjectFactory_app_web_n1mhegpg是如何定义的。

       1: internal class FastObjectFactory_app_web_n1mhegpg
       2: {
       3: // Methods
       4: private FastObjectFactory_app_web_n1mhegpg()
       5: {
       6: }
       7:  
       8: private static object Create_ASP_part_i_page1_aspx()
       9: {
       10: return new part_i_page1_aspx();
       11: }
       12:  
       13: private static object Create_ASP_part_i_page2_aspx()
       14: {
       15: return new part_i_page2_aspx();
       16: }
       17: }

      alt

      通过上面的Code,我们可以看到在FastObjectFactory中定义一系列的Create_ASP_XXX(后缀就是Page 编译生成的Type的名称)。通过这些方法,可以快速生成对一个的Page。至于为什么会叫作FastObjectFactory,我想是因为直接通过调用这个静态的方法快速地创建Page对象,从而避免使用Reflection的late binding带来的性能的影响吧。

      五、Preservation Files

      进行每一次编译,ASP.NET会生成一系列具有.compiled扩展名的保留文件(Preservation File)。这个文件非常重要,我们现在来深入介绍这个样一个文件。

      Preservation File这个文件本质上是一个XML。它是基于每个Page的,也就是每个Page都会有一个这样的Preservation File. 无论Page对应的Directory是怎样的,与之对应的Preservation File总会保存在根目录下,所以必须有一种机制保持为处于不同Directory的Page生成的Preservation File具有不同的文件名,不管Page的名称是否一样。所以Preservation File采用下面的命名机制:

       1: [page].aspx.[folder-hash].compiled

      其中[page]是Page的名称,[folder-hash]是对page所在路径的Hash Value。这样做有两个好处

      • 保证处于同一级目录的所有Preservation File具有不同的文件名。
      • 保证ASP.NET对于一个Http request可以找到Page对应的Preservation File。

      下面这个Preservation File就是上面Sample中Part II/Page1.aspx对应的Preservation File,名称为default2.aspx.cdcab7d2.compiled。我们来看看XML每个Item各代表什么意思。

       1: <?xml version="1.0" encoding="utf-8"?>
       2: <preserve resultType="3" virtualPath="/Artech.ASPNETDeployment/Part II/Page1.aspx" hash="fffffff75090c769" filehash="5f27fa390c45c52a" flags="110000" assembly="App_Web_hitlo3dt" type="ASP.part_ii_page1_aspx">
       3: <filedeps>
       4: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx" />
       5: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx.cs" />
       6: </filedeps>
       7: </preserve>
       8: 

      alt

      有这段XML我们可以看到,所有的内容都包含在preserve XML element中,在他中间定义了几个重要的属性.

      • virtualPath: Page的虚拟地址。

      • assembly:Assembly名称

      • Type:Page的编译后对应的Type(Http handler)。

      • hash: 一个代表本Preservation File状态的Hash value。

      • filehash: 一个代表本Dependent File状态的Hash value。

      通过hash和filehash的缓存,ASP.NET可以判断自上一次使用以来,Preservation File和它所依赖的Dependent File是否被改动,如果真的被改动,将会重新编译。因为对于文件的任何改动都会导致该Hash value的改变。

      此外,Preservation File的<filedeps>列出了所有依赖的文件,对于Page,一般是aspx和code behind。

      六、进一步剖析整个动态编译过程

      现在我们来总结整个动态编译的过程:

      • Step1:当ASP.NET收到对于某个Page的Request,根据这个request对应的Url试着找到该page对应的Preservation File,如果没有找到,重新编译Page所在目录下的所有需要编译的文件,同时生成一些额外的文件,包括Preservation File。
      • Step2:然后解析这个Preservation File,通过hash和filehash判断文件自身或者是Dependent File是否在上一次编译之后进行过任何的修改,如果是的,则重新编译。然后重新执行Step2。
      • Step3:通过Preservation File 的assembly attribute Load对应的assembly(如果Assembly还没有被Load的话),通过type获知对应的aspx type,然后借助FastObjectFactory的对应的Create_ASP_XXX创建这个type。这个新创建的对象就是我们需要的Http Handler,在之上执行相应的操作把结果Response到客户端。

      对动态编译的讨论就到这里,在本篇文章下半部分将会讨论另一种更加有用的编译方式:《深入剖析ASP.NET的编译原理之二:预编译(Precompilation)

      Microsoft 的Visual Studio为我们在应用开发中提供的强大功能,我们是有目共睹。借助该工具,是我们的开发 显得更加高效而轻松。从Microsoft把这个IDE的名字从VS.NET 该为VS(比如原来的Visual Studio.NET 2003,现在的版本叫VS2005),可以MS对该IDE的期望和野心:MS要把它改造成一个万能的IDE。不过任何都有其两面性,对于我们广大的开发者来说,VS是我们的各种行为简单化,傻瓜化;但是在另一方面,他也会蒙蔽我们的眼睛,使我们对它背后做的事情视而不见。以我们的ASP.NET Website开发为例,编程、编译、部署都可以借助VS,有了VS一切显得如此简单,每个人都会做,但是我想很多一部分人对一个ASP.NET Website如何进行编译不会很了解。这篇文章就来谈谈背后的故事——ASP.NET是如何进行编译的。由于篇幅的问题整篇文章分两个部分:动态编译Dynamical Compilation和预编译(Precompilation)。

      一、动态编译的过程

      alt我们先来介绍在动态编译下的大体的执行流程:当ASP.NET收到一个基于某个page的request的时候,先判断该Page和相关的Source code是否编译过,如果没有就将其编译,如果已经编译,就是用已经Load的Assembly直接生成Page对象。在这里有下面几点需要注意:

      • 动态编译是按需编译的,ASP.NET只会编译和当前Request相关的aspx和code。
      • 动态编译是基于某个目录的,也就是说ASP.NET会把被请求的page所在的目录的所有需要编译的文件进行编译,并生成一个Assembly。
      • 除了编译生成的Assembly外,动态编译还会生成一系列的辅助文件。
      • 对相关文件的修改,会导致重新编译,但是修改对当前的Request不起作用。也就是说如果你对某个aspx进行修改,那么对于修改后抵达的Request,会导致重新编译,但是对于之前的Request使用的依然是原来编译好的Assembly。
      • 编译生成的文件被放在一个临时目录中,这个目录的地址为Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files。其具体的目录结构如右图所示。


      在Temporary ASP.NET Files下的Artech.ASPNETDeployment是IIS中Virtual Directory的名称,以下两级目录的名称由Hash value构成,所以编译生成的文件就保存在c6f16246目录下。这个目录你可以通过HttpRuntime.CodegenDir获得。Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files只是一个默认的临时目录,你可以在web.config中的compilation section中设置你需要的临时目录。

       1: <compilation tempDirectory="d:\MyTempFiles" />

      二、一个小例子解释动态编译是如何进行的

      现在我用一个Sample来一探ASP.NET是如何进行动态编译的,下图是整个项目在VS中的结构。在这个Sample中,我建立了一个Website,在根目录下创建了两个Page:Default和Default2。 在两个子目录Part I和Part II下分别创建了两个Web page:Page1和Page2。

      alt
      在App_Code目录中创建了一个Utility的static class。下面是它的定义:

       1: public static class Utility
       2: {
       3: public static string ReflectAllAssmebly()
       4: {
       5: StringBuilder refllectionResult = new StringBuilder();
       6:  
       7: foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
       8: {
       9: if (!assembly.FullName.Contains("App_Web"))
       10: {
       11: continue;
       12: }
       13:  
       14: refllectionResult.Append(assembly.FullName + "<br/>");
       15: Type[] allType = assembly.GetTypes();
       16: foreach (Type typeInfo in allType)
       17: {
       18: refllectionResult.Append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + typeInfo.Name + "<br/>");
       19: }
       20: }
       21:  
       22: return refllectionResult.ToString();
       23: }
       24: }

      逻辑很简单,对当前加载的所有相关的程序集(这些Assembly的Fullname以App_Web打头)进行Reflection,列出所有的类型。这个ReflectAllAssmebly将在5个Web page(Default Page和两对Page1&Page2)的Page_Load事件中被调用。

       1: protected void Page_Load(object sender, EventArgs e)
       2: {
       3: this.Response.Write(Utility.ReflectAllAssmebly());
       4: }

      Default是列出所有4Page对应的Link以便我们访问它们,在我们再进行编译的情况下在IE中输入对应的URL来访问Default Page。(其他Page的Html中不具有真正的内容,是一个空的page.)

      alt

      通过上面的显示,我们可以看到现在有一个Assembly:App_Web_wh7-uda5。该Asssembly定一个的Type有5个,  _Default和 default_aspx分别对应Default Page,而Default2和 default2_aspxDefault2 Page的。FastObjectFactory_app_web_wh7_uda5是很重要的Type,我将会在后面对其进行深入介绍。正如我们在上面说过的,动态编译是按需编译,现在我们对Default Page进行访问,由于这次对该Website的第一次访问,所有需要的Source Code,包括aspx,code behind都要进行编译。在这个Sample中,虽然我们并没有访问Default2 page,但是我们说过,动态编译是基于目录的,由于Default Page和Default2 Page都直接置于根目录下,所以ASP.NET会把根目录下的所有文件编译到一个Assembly中。由于Page1和Page2位于子目录Part I和Part II之下,所以不会参与编译。除非我们下载对它进行Request。

      alt

      我们现在来访问Part I下的Page1和Page2看看会有什么结果。我们会发现,两次Request获得的输出是一样的:
      通过上面的输出我们发现,当前AppDomain中被加载的Assembly多了一个:App_Web_n1mhegpg。我们可以通过定义在该Assembly中的Type的命名可以猜出该Assembly是对Part I 目录进行编译产生的。Page1和Page2的编译后的Type name变成了part_i_page1_aspx& Page1和part_i_page2_aspx& Page2。此外我们看到,该Assembly中依然有一个FastObjectFactory的Type:FastObjectFactory_app_web_n1mhegpg。在这里我需要特别指出的是,名称的后缀都是通过 Hash算法得到的。

      有了上面的理论和实验结果,我想这个时候,你肯定已经想到,如果我现在对Part II的Page1和Page2进行访问,输出结果会是什么样子了。alt
      如果这个时候,你查看临时目录(Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files)中该Website对应的子目录,已将会看到生成了一些列的文件。

      三、Page最终被转化成什么?

      我们现在来看看通过编译,一个Page到底转换成什么。我们以Part I/Page1为例。通过上面的Sampe,我们知道它最终变成了两个Type:Page1和part_i_page1_aspx。下面是Page1的定义:

       1: public class Page1 : Page, IRequiresSessionState
       2: {
       3: protected HtmlForm form1;
       4: protected void Page_Load(object sender, EventArgs e)
       5: {
       6: base.Response.Write(Utility.ReflectAllAssmebly());
       7: }
       8: protected HttpApplication ApplicationInstance
       9: {
       10: get
       11: {
       12: return this.Context.ApplicationInstance;
       13: }
       14: }
       15: protected DefaultProfile Profile
       16: {
       17: get
       18: {
       19: return (DefaultProfile)this.Context.Profile;
       20: }
       21: }
       22: }
      下面是part_i_page1_aspx的定义:
       1: [CompilerGlobalScope]
       2: public class part_i_page1_aspx : Page1, IHttpHandler
       3: {
       4: // Fields
       5: private static object __fileDependencies;
       6: private static bool __initialized;
       7:  
       8: // Methods
       9: public part_i_page1_aspx();
       10: private HtmlHead __BuildControl__control2();
       11: private HtmlTitle __BuildControl__control3();
       12: private HtmlLink __BuildControl__control4();
       13: private HtmlForm __BuildControlform1();
       14: private void __BuildControlTree(part_i_page1_aspx __ctrl);
       15: protected override void FrameworkInitialize();
       16: public override int GetTypeHashCode();
       17: public override void ProcessRequest(HttpContext context);
       18: }

      part_i_page1_aspx中定义了一个基于aspx的HttpHandler所需的所有操作,并且它继承了Page1。所以我们可以说Part I/Page1这个page 最终的体现为part_i_page1_aspx。进一步说,对Part I/Page1的Http Request,ASP.NET所要做的就是生成一个part_i_page1_aspx来Handle这个request就可以了。

      四、FastObjectFactory

      通过上面的一个简单的Sample,你已经看到每个Assembly中都会生成一个以FastObjectFactory作为前缀的Type。这是一个很重要的Type,从它的名称我们不难猜出它的作用:高效的生成对象。而生成的是什么样的对象呢?实际上就是基于每个aspx的Http request的Http handler,对于Part I/Page1就是我们上面一节分析的part_i_page1_aspx对象。我们现在通过Reflector查看我们生成的App_Web_n1mhegpg中的FastObjectFactory_app_web_n1mhegpg是如何定义的。

       1: internal class FastObjectFactory_app_web_n1mhegpg
       2: {
       3: // Methods
       4: private FastObjectFactory_app_web_n1mhegpg()
       5: {
       6: }
       7:  
       8: private static object Create_ASP_part_i_page1_aspx()
       9: {
       10: return new part_i_page1_aspx();
       11: }
       12:  
       13: private static object Create_ASP_part_i_page2_aspx()
       14: {
       15: return new part_i_page2_aspx();
       16: }
       17: }

      alt

      通过上面的Code,我们可以看到在FastObjectFactory中定义一系列的Create_ASP_XXX(后缀就是Page 编译生成的Type的名称)。通过这些方法,可以快速生成对一个的Page。至于为什么会叫作FastObjectFactory,我想是因为直接通过调用这个静态的方法快速地创建Page对象,从而避免使用Reflection的late binding带来的性能的影响吧。

      五、Preservation Files

      进行每一次编译,ASP.NET会生成一系列具有.compiled扩展名的保留文件(Preservation File)。这个文件非常重要,我们现在来深入介绍这个样一个文件。

      Preservation File这个文件本质上是一个XML。它是基于每个Page的,也就是每个Page都会有一个这样的Preservation File. 无论Page对应的Directory是怎样的,与之对应的Preservation File总会保存在根目录下,所以必须有一种机制保持为处于不同Directory的Page生成的Preservation File具有不同的文件名,不管Page的名称是否一样。所以Preservation File采用下面的命名机制:

       1: [page].aspx.[folder-hash].compiled

      其中[page]是Page的名称,[folder-hash]是对page所在路径的Hash Value。这样做有两个好处

      • 保证处于同一级目录的所有Preservation File具有不同的文件名。
      • 保证ASP.NET对于一个Http request可以找到Page对应的Preservation File。

      下面这个Preservation File就是上面Sample中Part II/Page1.aspx对应的Preservation File,名称为default2.aspx.cdcab7d2.compiled。我们来看看XML每个Item各代表什么意思。

       1: <?xml version="1.0" encoding="utf-8"?>
       2: <preserve resultType="3" virtualPath="/Artech.ASPNETDeployment/Part II/Page1.aspx" hash="fffffff75090c769" filehash="5f27fa390c45c52a" flags="110000" assembly="App_Web_hitlo3dt" type="ASP.part_ii_page1_aspx">
       3: <filedeps>
       4: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx" />
       5: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx.cs" />
       6: </filedeps>
       7: </preserve>
       8: 

      alt

      有这段XML我们可以看到,所有的内容都包含在preserve XML element中,在他中间定义了几个重要的属性.

      • virtualPath: Page的虚拟地址。

      • assembly:Assembly名称

        Microsoft 的Visual Studio为我们在应用开发中提供的强大功能,我们是有目共睹。借助该工具,是我们的开发 显得更加高效而轻松。从Microsoft把这个IDE的名字从VS.NET 该为VS(比如原来的Visual Studio.NET 2003,现在的版本叫VS2005),可以MS对该IDE的期望和野心:MS要把它改造成一个万能的IDE。不过任何都有其两面性,对于我们广大的开发者来说,VS是我们的各种行为简单化,傻瓜化;但是在另一方面,他也会蒙蔽我们的眼睛,使我们对它背后做的事情视而不见。以我们的ASP.NET Website开发为例,编程、编译、部署都可以借助VS,有了VS一切显得如此简单,每个人都会做,但是我想很多一部分人对一个ASP.NET Website如何进行编译不会很了解。这篇文章就来谈谈背后的故事——ASP.NET是如何进行编译的。由于篇幅的问题整篇文章分两个部分:动态编译Dynamical Compilation和预编译(Precompilation)。

        一、动态编译的过程

        alt我们先来介绍在动态编译下的大体的执行流程:当ASP.NET收到一个基于某个page的request的时候,先判断该Page和相关的Source code是否编译过,如果没有就将其编译,如果已经编译,就是用已经Load的Assembly直接生成Page对象。在这里有下面几点需要注意:

        • 动态编译是按需编译的,ASP.NET只会编译和当前Request相关的aspx和code。
        • 动态编译是基于某个目录的,也就是说ASP.NET会把被请求的page所在的目录的所有需要编译的文件进行编译,并生成一个Assembly。
        • 除了编译生成的Assembly外,动态编译还会生成一系列的辅助文件。
        • 对相关文件的修改,会导致重新编译,但是修改对当前的Request不起作用。也就是说如果你对某个aspx进行修改,那么对于修改后抵达的Request,会导致重新编译,但是对于之前的Request使用的依然是原来编译好的Assembly。
        • 编译生成的文件被放在一个临时目录中,这个目录的地址为Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files。其具体的目录结构如右图所示。


        在Temporary ASP.NET Files下的Artech.ASPNETDeployment是IIS中Virtual Directory的名称,以下两级目录的名称由Hash value构成,所以编译生成的文件就保存在c6f16246目录下。这个目录你可以通过HttpRuntime.CodegenDir获得。Windows Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files只是一个默认的临时目录,你可以在web.config中的compilation section中设置你需要的临时目录。

         1: <compilation tempDirectory="d:\MyTempFiles" />

        二、一个小例子解释动态编译是如何进行的

        现在我用一个Sample来一探ASP.NET是如何进行动态编译的,下图是整个项目在VS中的结构。在这个Sample中,我建立了一个Website,在根目录下创建了两个Page:Default和Default2。 在两个子目录Part I和Part II下分别创建了两个Web page:Page1和Page2。

        alt
        在App_Code目录中创建了一个Utility的static class。下面是它的定义:

         1: public static class Utility
         2: {
         3: public static string ReflectAllAssmebly()
         4: {
         5: StringBuilder refllectionResult = new StringBuilder();
         6:  
         7: foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
         8: {
         9: if (!assembly.FullName.Contains("App_Web"))
         10: {
         11: continue;
         12: }
         13:  
         14: refllectionResult.Append(assembly.FullName + "<br/>");
         15: Type[] allType = assembly.GetTypes();
         16: foreach (Type typeInfo in allType)
         17: {
         18: refllectionResult.Append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + typeInfo.Name + "<br/>");
         19: }
         20: }
         21:  
         22: return refllectionResult.ToString();
         23: }
         24: }

        逻辑很简单,对当前加载的所有相关的程序集(这些Assembly的Fullname以App_Web打头)进行Reflection,列出所有的类型。这个ReflectAllAssmebly将在5个Web page(Default Page和两对Page1&Page2)的Page_Load事件中被调用。

         1: protected void Page_Load(object sender, EventArgs e)
         2: {
         3: this.Response.Write(Utility.ReflectAllAssmebly());
         4: }

        Default是列出所有4Page对应的Link以便我们访问它们,在我们再进行编译的情况下在IE中输入对应的URL来访问Default Page。(其他Page的Html中不具有真正的内容,是一个空的page.)

        alt

        通过上面的显示,我们可以看到现在有一个Assembly:App_Web_wh7-uda5。该Asssembly定一个的Type有5个,  _Default和 default_aspx分别对应Default Page,而Default2和 default2_aspxDefault2 Page的。FastObjectFactory_app_web_wh7_uda5是很重要的Type,我将会在后面对其进行深入介绍。正如我们在上面说过的,动态编译是按需编译,现在我们对Default Page进行访问,由于这次对该Website的第一次访问,所有需要的Source Code,包括aspx,code behind都要进行编译。在这个Sample中,虽然我们并没有访问Default2 page,但是我们说过,动态编译是基于目录的,由于Default Page和Default2 Page都直接置于根目录下,所以ASP.NET会把根目录下的所有文件编译到一个Assembly中。由于Page1和Page2位于子目录Part I和Part II之下,所以不会参与编译。除非我们下载对它进行Request。

        alt

        我们现在来访问Part I下的Page1和Page2看看会有什么结果。我们会发现,两次Request获得的输出是一样的:
        通过上面的输出我们发现,当前AppDomain中被加载的Assembly多了一个:App_Web_n1mhegpg。我们可以通过定义在该Assembly中的Type的命名可以猜出该Assembly是对Part I 目录进行编译产生的。Page1和Page2的编译后的Type name变成了part_i_page1_aspx& Page1和part_i_page2_aspx& Page2。此外我们看到,该Assembly中依然有一个FastObjectFactory的Type:FastObjectFactory_app_web_n1mhegpg。在这里我需要特别指出的是,名称的后缀都是通过 Hash算法得到的。

        有了上面的理论和实验结果,我想这个时候,你肯定已经想到,如果我现在对Part II的Page1和Page2进行访问,输出结果会是什么样子了。alt
        如果这个时候,你查看临时目录(Directory\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files)中该Website对应的子目录,已将会看到生成了一些列的文件。

        三、Page最终被转化成什么?

        我们现在来看看通过编译,一个Page到底转换成什么。我们以Part I/Page1为例。通过上面的Sampe,我们知道它最终变成了两个Type:Page1和part_i_page1_aspx。下面是Page1的定义:

         1: public class Page1 : Page, IRequiresSessionState
         2: {
         3: protected HtmlForm form1;
         4: protected void Page_Load(object sender, EventArgs e)
         5: {
         6: base.Response.Write(Utility.ReflectAllAssmebly());
         7: }
         8: protected HttpApplication ApplicationInstance
         9: {
         10: get
         11: {
         12: return this.Context.ApplicationInstance;
         13: }
         14: }
         15: protected DefaultProfile Profile
         16: {
         17: get
         18: {
         19: return (DefaultProfile)this.Context.Profile;
         20: }
         21: }
         22: }
        下面是part_i_page1_aspx的定义:
         1: [CompilerGlobalScope]
         2: public class part_i_page1_aspx : Page1, IHttpHandler
         3: {
         4: // Fields
         5: private static object __fileDependencies;
         6: private static bool __initialized;
         7:  
         8: // Methods
         9: public part_i_page1_aspx();
         10: private HtmlHead __BuildControl__control2();
         11: private HtmlTitle __BuildControl__control3();
         12: private HtmlLink __BuildControl__control4();
         13: private HtmlForm __BuildControlform1();
         14: private void __BuildControlTree(part_i_page1_aspx __ctrl);
         15: protected override void FrameworkInitialize();
         16: public override int GetTypeHashCode();
         17: public override void ProcessRequest(HttpContext context);
         18: }

        part_i_page1_aspx中定义了一个基于aspx的HttpHandler所需的所有操作,并且它继承了Page1。所以我们可以说Part I/Page1这个page 最终的体现为part_i_page1_aspx。进一步说,对Part I/Page1的Http Request,ASP.NET所要做的就是生成一个part_i_page1_aspx来Handle这个request就可以了。

        四、FastObjectFactory

        通过上面的一个简单的Sample,你已经看到每个Assembly中都会生成一个以FastObjectFactory作为前缀的Type。这是一个很重要的Type,从它的名称我们不难猜出它的作用:高效的生成对象。而生成的是什么样的对象呢?实际上就是基于每个aspx的Http request的Http handler,对于Part I/Page1就是我们上面一节分析的part_i_page1_aspx对象。我们现在通过Reflector查看我们生成的App_Web_n1mhegpg中的FastObjectFactory_app_web_n1mhegpg是如何定义的。

         1: internal class FastObjectFactory_app_web_n1mhegpg
         2: {
         3: // Methods
         4: private FastObjectFactory_app_web_n1mhegpg()
         5: {
         6: }
         7:  
         8: private static object Create_ASP_part_i_page1_aspx()
         9: {
         10: return new part_i_page1_aspx();
         11: }
         12:  
         13: private static object Create_ASP_part_i_page2_aspx()
         14: {
         15: return new part_i_page2_aspx();
         16: }
         17: }

        alt

        通过上面的Code,我们可以看到在FastObjectFactory中定义一系列的Create_ASP_XXX(后缀就是Page 编译生成的Type的名称)。通过这些方法,可以快速生成对一个的Page。至于为什么会叫作FastObjectFactory,我想是因为直接通过调用这个静态的方法快速地创建Page对象,从而避免使用Reflection的late binding带来的性能的影响吧。

        五、Preservation Files

        进行每一次编译,ASP.NET会生成一系列具有.compiled扩展名的保留文件(Preservation File)。这个文件非常重要,我们现在来深入介绍这个样一个文件。

        Preservation File这个文件本质上是一个XML。它是基于每个Page的,也就是每个Page都会有一个这样的Preservation File. 无论Page对应的Directory是怎样的,与之对应的Preservation File总会保存在根目录下,所以必须有一种机制保持为处于不同Directory的Page生成的Preservation File具有不同的文件名,不管Page的名称是否一样。所以Preservation File采用下面的命名机制:

         1: [page].aspx.[folder-hash].compiled

        其中[page]是Page的名称,[folder-hash]是对page所在路径的Hash Value。这样做有两个好处

        • 保证处于同一级目录的所有Preservation File具有不同的文件名。
        • 保证ASP.NET对于一个Http request可以找到Page对应的Preservation File。

        下面这个Preservation File就是上面Sample中Part II/Page1.aspx对应的Preservation File,名称为default2.aspx.cdcab7d2.compiled。我们来看看XML每个Item各代表什么意思。

         1: <?xml version="1.0" encoding="utf-8"?>
         2: <preserve resultType="3" virtualPath="/Artech.ASPNETDeployment/Part II/Page1.aspx" hash="fffffff75090c769" filehash="5f27fa390c45c52a" flags="110000" assembly="App_Web_hitlo3dt" type="ASP.part_ii_page1_aspx">
         3: <filedeps>
         4: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx" />
         5: <filedep name="/Artech.ASPNETDeployment/Part II/Page1.aspx.cs" />
         6: </filedeps>
         7: </preserve>
         8: 

        alt

        有这段XML我们可以看到,所有的内容都包含在preserve XML element中,在他中间定义了几个重要的属性.

        • virtualPath: Page的虚拟地址。

        • assembly:Assembly名称

        • Type:Page的编译后对应的Type(Http handler)。

        • hash: 一个代表本Preservation File状态的Hash value。

        • filehash: 一个代表本Dependent File状态的Hash value。

        通过hash和filehash的缓存,ASP.NET可以判断自上一次使用以来,Preservation File和它所依赖的Dependent File是否被改动,如果真的被改动,将会重新编译。因为对于文件的任何改动都会导致该Hash value的改变。

        此外,Preservation File的<filedeps>列出了所有依赖的文件,对于Page,一般是aspx和code behind。

        六、进一步剖析整个动态编译过程

        现在我们来总结整个动态编译的过程:

        • Step1:当ASP.NET收到对于某个Page的Request,根据这个request对应的Url试着找到该page对应的Preservation File,如果没有找到,重新编译Page所在目录下的所有需要编译的文件,同时生成一些额外的文件,包括Preservation File。
        • Step2:然后解析这个Preservation File,通过hash和filehash判断文件自身或者是Dependent File是否在上一次编译之后进行过任何的修改,如果是的,则重新编译。然后重新执行Step2。
        • Step3:通过Preservation File 的assembly attribute Load对应的assembly(如果Assembly还没有被Load的话),通过type获知对应的aspx type,然后借助FastObjectFactory的对应的Create_ASP_XXX创建这个type。这个新创建的对象就是我们需要的Http Handler,在之上执行相应的操作把结果Response到客户端。

        对动态编译的讨论就到这里,在本篇文章下半部分将会讨论另一种更加有用的编译方式:《深入剖析ASP.NET的编译原理之二:预编译(Precompilation)

Tags: