在本篇文章的第一部分:[原创]深入剖析ASP.NET的编译原理之一:动态编译(Dynamical
Compilation),详细讨论了ASP.NET如何进行动态编译的,现在我们来谈谈另外一种重要的编译方式:预编译
(Precompilation)。
1.为什么要进行预编译
ASP.NET
2.0的编译方式大体可以分成两种:动态编译和预编译,要回答为什么要进行预编译,我们先要看看动态编译有什么不好的地方。我们回顾一下上一篇介绍的
ASP.NET进行动态编译的简单的流程:当来自Brower的一个基于aspx的Http request抵达Web server,IIS
handle这个request,通过分析注册在IIS中的Application Mapping,将Request
传给aspnet_isapi.dll ISAPI extension。ISAPI extension通过HttpRuntime进入Http
Runtime
Pipeline,HttpRuntime为每个Request创建一个单独的HttpContext对象,用于保存request的Context信
息。在Http Runtime Pipeline中,Http request会被注册的一系列的Http
module处理,比如OutputCache Module,Session Module,Authentication
Module,AuthorizationErrorHandler
Module等等。在Pipeline的终端,ASP.NET需要需要根据request创建对应的HttpHandler对象来处理该Request,
并生成结果Response到Client。对于一个基于Aspx的Http request,对应的Http
handler对象一般就是一个System.Web.UI.Page对象。
ASP.NET会先判断对应的Page
type是否存在于被Cache的Assembly中,如果存在,直接创建Page对象,否则ASP.NET会先对该Page的相关的Source
code (包括code behind,html等等)
进行编译,我们也说过这种编译是一Directory为单位的,也就是说,处于同一个Directory下的需要编译的文件会被编译成到同一个
Assembly中。编译生成的Assembly会被Cache,用于后续的Request。
正是因为对资源的首次访问会导致一次编译(这样说不太准确,因为动态编译是以directory为单位进行的,应该对对某个Directory下的资源进
行首次访问),这样会严重降低Web Application的响应速度。所以我们为了避免这种情况,需要预先对web
site进行编译,所以提高web site的响应是进行预编译的最重要的原因。
同时动态编译就以为着Web server上放置的是Source code,而且他们是可被修改的。而对于一个开发完毕的Web
Application,我们更希望以Binary Assembly的方式进行部署,这样Server上部署的都是Binary
Assembly,不怕被别人篡改而导致系统的崩溃,从知识产权来讲,也更利于保护商业秘密。这也是我们为什么要进行预编译的另一个原因。
下面我们就来讲讲如何进行预编译,以及与编译背后的原理。同时在这里我需要特别提出的是,在上一部分讲的一些术语和原理,比如Preservation
file,FastObjectFactory,同样适用于预编译,重复的内容,在这里就不必再介绍了。同时我也将沿用上一部的Sample。如果想看看
相关的内容,请参阅[原创]深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation)。
2.In Place Pre-compilation V.S. Pre-compilation for
Deployment
对于预编译,有可以分为In Place Pre-compilation和Pre-compilation for Deployment,In
Place Pre-compilation很简单,实际上就是把整个Web
site编译到我们一个临时的目录下面,这个临时目录也就是我们在介绍动态编译提到的那个临时目录。而且这个编译的方式,包括生成的文件也和动态编译完全
一样,唯一不同就是编译的时间:预先编译,编译的范围:整个Web site。这种编译就是你常用的在VS的build。这种编译方式一般用于开发阶段。
为了部署为目的的编译是我们今天讨论的重点,下面我们就着重来讨论Pre-compilation for Deployment。
注:在ASP.NET的编译都是通过一个叫做aspnet_compiler的工具执行的,该工具随ASP.NET
2.0一起发布,你完全可以利用此工具以命令行的方式的执行编译,并通过传递不同的命令行开关设置不同的编译选项。该工具被置于了VS中,使你可以利用
VS进行可视化的编译。
3.Non-updatable Pre-compilation V.S. Updatable
Pre-compilation
ASP.NET 2.0为我们提供了几种不同方式的预编译和部署。为了弄清楚这些预编译和部署方式,我们先来回顾一下ASP.NET
1.x下的编译方式。我们知道在ASP.NET 1.x时代对整个Web
site进行编译,实际上我们只会对所有C#和VB.NET等后台代码进行编译,并生成一个单一的Assembly。而Web
page的aspx是不会参与编译的。所以当我们访问一个Web page的时候,ASP.NET必须对aspx进行动态编译。
这一切之所以能够进行是因为Web page采用的是aspx + code behind的模式。
<%@ Page Language="C#" AutoEventWireup="false"
Codebehind="Default.aspx.cs" Inherits="Default" %>
public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
}
从上面我们可以看到aspx和Code behind是一种继承的关系,aspx继承和它对应的Code
Behind。ASP.NET可以把Code
behind和aspx分开进行编译,把它们编译到不同的Assembly中。我们就是上面的Code为例,我们现在若对该Web
site进行编译的话,Default.aspx.cs会被编译到一个Assembly中,假设这个Assembly为App_Web.dll.
我们把该Dll和aspx部署到Production
Server上。如果我们现在访问defaut.aspx。ASP.NET会对aspx进行动态编译,生成的Assembly可以暂时成为
App_Web_aspx.dll。对于Default.aspx,如果我们如C#代码来描述的话,应该像下面一样定义:
public class default_aspx:Default
{
}
这种编译方式,我自己把它叫做对asXx的动态编译。在ASP.NET2.0 中也沿用了这种编译方式。这种编译方式的主要特征是对Code
behind和所有的后台代码进行预编余,aspx(确切地说应该是asXx:asax,asmx,asax等)原样部署。由于这种方式的预编
译,asXx是可以修改的(当然这种修改是有一定限制的,因为code behind已经编译好了,所以这种修改只可能是和code
behind无关的修改),所以又叫做Updatable Pre-compilation。
除了Updatable Pre-compilation之外,ASP.NET还提供另外一种高效的预编译方式,Non-updatable
Pre-compilation,之所以叫做不可修改的预编译,这是因为:这种编译方式把asXx、Code
behind、后台代码甚至是部分Resource都进行预编译,从而避免了运行时对asXx的动态编译,从而最大程度地提高了整个Web
site的响应。在部署的时候,我们除了把生成的Assembly进行部署之外,所有的通过编译生成的asXx也必须进行部署。
不过需要特别说明的是,此时的asXx文件仅仅是一个占位的文件而已,它里面不具有任何HTML。
4.Partial class
在ASP.NET 1.x,由于采用的aspx + code behind的机制,对于任何一个Web page或者其他ASP.NET
基于axXx的对象来说,都是由两个文件、两个class组成。两个文件是指axXx和code behind,两个class是指Code
behind定义的继承自System.Web.UI.Page的class,和一个继承自它的由axXx生成的class。
对于使用过ASP.NET 1.x来说,一定会很熟悉这样一种情况:对于每个在aspx中通过HTML定义的Server Control,在Code
behind中必须具有一个对应的protected成员,否则你不能通过编程的方式访问这个Server
control。以不同方式呈现的同一个Server control通过ID关联起来,如果在Code behind中改了Server
control的ID,Server control的Server端的Event handler将会失去原有的作用。
但是在ASP.NET 2.0来说,这种情况发生了改变,在aspx中的Server control在Code
behind中却没有相应的成员变量,但是我们可以毫无障碍地访问到每个Server control。这使得我们的code
behind更加简洁,通过避免了Server control在aspx和code bebind中的不匹配的问题。这一切都得益于.NET
Framework 2.0提供的partial
class的机制:把同一个class分布于不同文件中进行定义。有了这个概念,我们来看ASP.NET 2.0的code behind机制。
比如我们有这样的一个Page:
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="Default.aspx.cs" Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Default</title>
<link href="Style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Button runat="server" ID="btnRefresh" Text="Refresh"
OnClick="btnRefresh_Click" />
</div>
</form>
</body>
</html>
Code behind如下:
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void btnRefresh_Click(object sender, EventArgs e)
{
this.Response.Write("The click event of \"Refresh\" button is
fired");
}
}
而实际上,ASP.NET会为我们创建一个隐藏的.cs文件(这个文件有人 把它称之为Sibling partial class):
public partial class _Default: IRequiresSessionState
{
protected Button _ btnRefresh;
}
这个文件会随着aspx文件的改变而动态变化,所以code behind中的Server control永远和aspx中的Server
control是完全匹配的。所以我们说ASP.NET 2.0的Page是由3个文件、两个class组成的。
5.编译的粒度和Assembly的命名
到现在为止,我们所讲的ASP.NET的预编译都是以Directory为单位的,同一个Directory下的所有需要编译的文件被编译到同一个
Assembly中。ASP.NET还支持以Page为单位的预编译,也就是每个Page编译成一个Assembly。
在默认的情况下,ASP.NET预编译生成的Assembly名称是随机生成的,也就是每次生成的Assembly都具有不同的name。所以我们在部署
Web
site的时候,一般需要把原来的Assembly删掉,再部署新的Assembly。不过ASP.NET为我们提供了另外一种选择,使得每次编译生成的
Assembly具有相同的名称,这样我们部署的时候就可以直接把新的Assembly 拷贝到Production
Server上,自动覆盖掉同名的Assembly。
6. Sample
我们沿用上一部份是用的Sample,我们通过采用不同的预编译方式看看程序将如何运行。
6.1 Non-updatable Pre-compilation
我们采用如上图所示的默认的发布方式,ASP.NET 将会进行Non-updatable
Pre-compilation。浏览目标文件夹,我们会发现如下的文件结构.
除了多了一个Bin目录和PrecompiledApp.config之外,整个结构和Source
code中的结构完全一样。通过上面的分析,我们知道这种预编译方式是将asXx、code
behind、后台代码已经Resource一起编译成Assembly。我们说过对于这样的预编译方式,aspx仅仅是一个站位的文件而以,其中
HTML已经没有任何意义了,那么对于编译后的aspx中到底是什么东西呢。我们来一探究竟。打开每个aspx都是一段如下如下一样文字,并无任何
HTML。
This is a marker file generated by the precompilation
tool, and should not be deleted!
PrecompiledApp.config里面具有一段简短的configuration,表明version和是否可以进行进一步的修改。
<precompiledApp version="2" updatable="false"/>
所有的Assembly被编译到Bin目录中,我们来看看到底生成了一些什么样的文件在Bin目录中。
在Bin目录由两类文件构成:Assembly和以complied作为扩展名的Preservation file。Preservation
file的内容和作用在第一部分已经详细介绍过了,相信大家不会感到陌生。Preservation
file在这里和动态编译所起的作用一样。唯一有一点不同的是,他的结构更加简洁,去掉的Dependence
file的列表,因为对于Non-updatable Pre-compilation来说,每个Page的以来的文件都是不可更改的。
<preserve resultType="6"
virtualPath="/Artech.ASPNETDeployment/App_Code/" hash="439abe7d"
filehash="" flags="140000" assembly="App_Code" />
我们来运行以下程序,和动态编译情况下的输出结果比较,看看有什么不同。我们照例先运行Default Page。
输出的结果印证我们前面的讨论:处于同一个目录下的Default
和Default2被编译到同一个Assembly中,关注于处理逻辑的code behind的class
name为Default和Default2,关注与可视化界面render的aspx对应的class
name被加上的_aspx后缀,如果对default_aspx和default2_aspx进行Reflect的话,你会发现他们分别继承
Default和Default2,而后者直接继承自System.Web.UI.Page。所以default_aspx和default2_aspx
是真正的意义上基于Web page的Http
handler。像动态编译一样,预编译生成一个基于Assembly的FastObjectFactory Type,对该对象的描述请参照第一部分。
有了前面的理论基础,相信大家已经猜到这时候,我浏览Part I下的Page1和Page2时的输出是什么
样子,由于预编译是以目录为单位的,我们对Part
I下的任何一个page的访问,都会加载相同的Assembly,所以此时对这两个Page的访问会得到一样的输出结果:
6.2 Updatable Pre-compilation
接下来我们来对Web Page进行Updatable Pre-compilation,相关的编译设置如下:选择Allow this
precompiled site to be updatable。
生成的文件及其结构和进行Non-updatable Pre-compilation,不同的又一下3点:
1、PrecompiledApp.config:updatable被设置为true。
<precompiledApp version="2" updatable="true"/>
2、asXx和我们进行开发时内容一样,你如aspx包含的就是HTML,我们可以在部署之后对他们进行和code behind无关的修改。
3、Preversation file中有加上了Page对应的dependence file列表。
<?xml version="1.0" encoding="utf-8"?>
<preserve resultType="9"
virtualPath="/Artech.ASPNETDeployment/App_GlobalResources/"
hash="439abe7d" filehash="ff21249472dbf6cb" flags="140000"
assembly="App_GlobalResources" resHash="1cba48dd56e28538" />
我们来运行一下Web site,看看现在的输出结果又有何不同。首先打开Default Page:
通过上图,我们发现此时加载了两个相关的Assembly。我们来分析一下为什么会这样。在分析Updatable
Pre-compilation时,我们说过:asXx是不会才与编译的,只有他们的code behind,
所有的后台代码,资源文件才会本编译。对于一个page 来说,page的code
behind被编译到Assembly中,aspx则不会。Aspx在运行时实行动态编译,所以aspx是可被修改的。在本例中,我们访问Default
Page,ASP.NET先对aspx进行编译,其对应的class
name为default_aspx,由于default_aspx继承与Default,并且Default存在于预编译生成的Assembly中,所
以这个Assembly被加载进来。
由于同一个page最重本编译到两个不同的Assembly中,所以我们此时访问Part
I中的Page1或者Page2,又会有两个Assembly被加载进来:
6.3 以Page为单位进行预编译
前面我们进行的都是以directory为单位的预编译,现在我们缩小编译的粒度,以Page为单位进行编译。我们选择了“Use fixed
naming and single page assemblies”选项。那么现在进行的是 基于单个page的non-updatable
pre-compilation。通时由于采用的是fixed naming的编译方式,每次进行编译生成的Assembly的名称都是一样的。
现在我们来看看,编译之后生成的Assembly:
我们看到编译器为每个Page生成了一个单独的Assembly。此时运行程序,你看到的又将不同。如果此时你访问Default
Page,你将看到:
是不是和上面不同,Assembly只有Default对应的两个Type,没有了处于同一个目录下的Default2的Type。因为
Default2有它独自的Assembly
.所以你该会想到,如果我们现在每访问一个没有被访问过的Page,就会有一个新的Assembly被加载。比如我么访问Part I下的Page1:
6.4编译强类型的Assembly
我们知道可以通过一个Public key/Private key pair对Assembly进行签名,进而把它部署到GAC中,我们来看看如何做。
首先我通过SN.exe生成Public key/Private key
pair并保存到一个文件中(比如D:\MyKey.keys),然后进行如下的编译设置
那么我们进行编译就会生成强类型的Assembly。我们可以运行我们的程序来证明:
每个Assembly有具有一样的PublicKeyToken,因为我们使用的一样的Public key/Private key
pair进行对每个Assembly签名的。