摘要: Inside ASP.NET 2.0-即时编译系统
Inside ASP.NET 2.0-即时编译系统
文/ 黄忠成(原文刊登于Run! PC)
从ASP.NET 1.1 到2.0, 编译系统的进化
在笔者撰写『深入剖析ASP.NET 元件设计』一书时,曾相当深入的探讨ASP.NET 1.1 的即时编译模型, 该章节以图1 为开端, 一步步的将隐身于后的设计理念摊开在讀者面前,时至今日,ASP.NET即将迈入2.0 了,这个即时编译模型做了相当大幅度的变化, 图2 是对照1.1 与2.0 的即时编译模型概观,讀者们可以发现,2.0的即时编译模型复杂了许多。 图1
图2
在1.1 时,当访问者要求一个文件时,ISAPIRuntime(IIS 的要求处理物件) 会依照文件類型來唤起适当的Http Handler ,以.aspx 來說就是PageHandlerFactory, 她也是即时编译系统的入口, 这段流程在2.0 仍然没有改变,但后面的动作就完全变样了,在1.1 时, PageHandlerFactory 会使用PageParser 來解译.aspx 文件,再交由PageCompiler 來产生出编译档案。在2.0 时,同样的动作是交由BuildManager 來完成,其会呼叫适当的BuildProvider 來处理要求的文件, 最后交由适当的Compiler 來产生编译档案。不知讀者们是否看出上面这段话所隐含的意义,是的!BuildManager 具备依照不同附档名使用不同BuildProvider 的能力, 这代表着设计者可能拥有撰写自订的BuildProvider 來參与即时编译流程。讀者们可以在Visual Web Developer 的New Items 选项中看到图3 的画面。 图3
其中最引人注意的是ASP.NET 2.0 允许使用者撰写Generic Handler, 也就是1.1 中的自定Http Handler 程式档,该Wizard 会产生出程式1 的码。
程式1
<%@ WebHandler Language="C#" Class="Handler" %>
using System.Web;
public class Handler : IHttpHandler {
public void ProcessRequest (HttpContext context) {
context.Response.ContentType = "text/plain";
context.Response.Write("Hello World");
}
public bool IsReusable {
get {
return false;
}
}
}
你是否看到了一个介于ASP.NET Script 与一般程式档的怪異程式码呢? 在存档后执行时会看到图4 的结果。
图4
问题來了, 以往撰写这种自定义的Http Handler 时,设计师必须预先将程式码编译好, 放置于网站目錄下, 这个Handler 才能正常运作, 但现在并未执行这个编译动作啊?那是谁为我们编译这个档案,又是如何做的呢? 答案与1.1 时相同,就是SimpleHandlerFactory, 但后面的动作就不同了, 以往的SimpleHandlerFactory 只是载入对应的Assembly 來启动Http Handler, 在2.0 时此动作换成了BuildManager, 她会寻找.ashx 对应的Build Provider,也就是WebHandlerBuildProvider 來运行即时编译模型。以上的讨論說明一件事,Handler Factory 的大部份工作已经下放给BuildManager 了, 而目的就是提供一个更强大的即时编译模型,不只是可以编译.aspx、.ascx, 还可以编译各式各样的文件,例如.masterpage 也是这个模型中的一员, 这带來了一个难以想象的极大优点,设计师以后将具备自定Script 文件的能力,只要有需求,设计师可以自行定义一种Script 语言, 再提供对应的BuildProvider 物件,BuildManager 将很樂意的为你完成即时编译动作,而且优点还不只于此,BuildManager支援预编译模型, 也就是說设计师只要提供Script 文件与BuildProvider 后, 就能享受即时编译与预编译兩种模型。举一个较实务的例子, 一个设计师希望提供某种较简单的Script 语言供使用者应用, 那么该设计师只需提供一个MyScriptBuildProvider, 将其与特定的附档名对应之后, 再利用CodeDom 來产生真正的程式码就可以了,接下來的动作BuildManager 将很樂意的帮你完成。
Reloaded! Page Compiler-Time
既然2.0 已经改变即时编译模型了,那么就让我们从新了解这个编译系统究竟是如何
动作的,在1.1 中,即时编译系统最令人注意的是PageParser 物件,此物件会讀入.aspx
文件,将其解译成一群Control Builder 物件交由PageCompiler 物件來产生原始程式码后
编译,这段过程在2.0 中依然没变,不同的是PageParser 在2.0 中已经不是由
PageHandlerFactory 來呼叫了,图5 是2.0 中Web Page 的编译时期概观。
图5,一段不算短的旅行景点, 在BuildManager 接到编译命令时, 会先将目錄中的几个外部档案编译好, 这些档案就是Resource 、Web Reference 、Code 、Profile 、Global.asax,Resource 指的是资源档,Web Reference 通常是引用Web Services 时用的档案,Code 是位于Code 目錄下的程式档,Profile 则是位于web.config 中的Profile 区段定义,Global Asax 则是大家所熟悉的Global.asax 档案。接下來是本节的重头戏了,Compiling Web Files!这个动作将会编译网站中的.aspx 或是其它拥有相对应BuildProvider 物件的档案,例如.ashx与.masterpage 等等。回到Page 的编译周期上,.aspx 所对应的BuildProvider 是PageBuilderProvider 物件,此物件会使用PageParser 來解译.as px,再利用PageCodeDomTreeGenerator 來产生出原始码,最后交由适当的Compiler 编译。
Manager、Provide r、Generator
承上节,BuildManager与BuildProvider 及其CodeGenerator 拥有不可分的关系,图6 是目前2.0 所提供的一部份BuildProvider 物件,讀者们可以在其中发现许多熟悉的物件名称,她们就是对应到目前你能在ASP.NET 中撰写的文件。
图6
有趣的是Page Theme 居然也拥有一个PageThemeBuilderProvider, 这代表着什么呢?笔者原來以为Theme 只是一个简单的文字档,当Page 套用某一个Theme 时,只是由该文字档中讀取定义來套用至控件上,但结果不然, 由PageThemeBuilderProvider 的出现來看,Theme 是一个编译后的文件,PageThemeBuilderProvider 会编译所有的Theme 档案, 也就是.skin,事实上,所有于内的控件定义都会被编译成控件实体,当Page 需要套用某个Theme 至控件时, 只是将控件的属性复制过來罢了, 没有解译动作, 速度自然快上不少。基本上,所有可编译型的BuildProvider 物件都会提供兩个物件,一个是Parser,用來解译文件用, 另一个是CodeDomTreeGenerator, 用來将Control Builder 物件群转换为可编译的原始码, 以PageTheme 來說, 就如图7 所示。 图7
当然,这并不是說每一个BuildProvider 都得提供这些东西,契约层仅到达BuildProvider 就停止了, 只要该BuildProvider 能传回一个实体,BuildManager 并不管其内部是如何达到的。
预编译系统
截至目前为止, 我们一直在即时编译系统上打转, 并未谈到另一个系统, 那就是预编译系统, 事实上这个系统只是即时编译系统的一种呈现型式, 当BuildManager 启动时,会先判别要求的目錄中是否拥有.compiled 的档案, 存在的话就将其视为预编译模式, 载入.compiled 文件中所定义的Assembly, 等会!预编译后的档案連真正的.aspx 都不存在了,BuildManager 如何做接下來的动作了,又是如何与BuildProviders 互动呢?哦, 没有!在预编译情况下,BuildManager 根本就不会用到BuildProvider, 这跟即时编译系统的二次动作一样, 当即时编译系统完成后, 会将结果存放到暂存目錄中, 顺带着也会放一份到Cache 中, 待下次收到要求时, 就直接取用了, 预编译系统只是跳过了第一次那一段动作而已, 这代表着, 自定的BuildProvider 不用做特别的动作, 就可以享受到预编译系统的优点。
Custom Build Provider
由于目前ASP.NET 2.0 仍处于Beta 版本,有关于Build Provider 的资讯少之又少,不过我还是在文件中找到程式2 的說明。程式2
<configuration> <system.web> <compilation> …….
<buildProviders> <buildProvider extension=".mafx" type="BuildProviderType, BuildProviderAssembly" /> </buildProviders>
</compilation> </system.web> </configuration>
粗体字的部份就是定义自定Build Provider 的地方,这可以证明在ASP.NET 2.0 中,设计师是被允许撰写Build Provider 的,不过除了这个文件外,我再也找不到更深入的资讯了,而很不幸的, 这个文件有一个错误, 其中的buildProvider 定义是不被接受的, 实际上的语法应该如程式3。程式3
<compilation debug="true">
<buildProviders>
<add extension=".ppp"
type="TestBuildProvider.MyCSharpBuilder, TestBuildProvider"
appliesTo="Code"/>
</buildProviders>
</compilation>
extension 是定义此BuildProvider 对应至何种副档名,type 指的是BuildProvider 的Assembly 与Type, 最后的appliesTo 是代表着使用于何种模式, 有四个选择, 一是Code, 代表程式档, 二是Resource, 就是资源档, 三是Web, 代表着网页档案(.aspx、.ascx…), 四是All, 代表任何類型档案。要撰写BuildProvider,讀者必须先准备Visual Studio 2005 Beta 或是Visual C# Express,这些工具才有提供Class Library 的Wizard ,否则就得使用Command Line 方式编译范例了,程式4 是我们的第一个BuildProvider 范例。程式4
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.IO; using System.CodeDom; using System.CodeDom.Compiler; using System.Web.Compilation; #endregion
namespace TestBuildProvider
{ public class MyCSharpBuilder:BuildProvider {
public override void GenerateCode(AssemblyBuilder assemblyBuilder)
{ TextReader reader = base.OpenReader(); string scriptString = reader.ReadLine(); CodeCompileUnit unit = new CodeCompileUnit(); unit.Namespaces.Add(new CodeNamespace("TEST")); CodeTypeDeclaration class1 = new CodeTypeDeclaration("HelloClass"); class1.IsClass = true; CodeMemberMethod method1 = new CodeMemberMethod(); method1.Name = "SayHello"; method1.ReturnType = new CodeTypeReference("System.String"); method1.Statements.Add(new CodeMethodReturnStatement(
new CodePrimitiveExpression(scriptString))); method1.Attributes = MemberAttributes.Public; class1.Members.Add(method1); unit.Namespaces[0].Types.Add(class1); assemblyBuilder.AddCodeCompileUnit(this, unit);
}
}
}
让我稍微解释一下这个范例, 程式中以CodeDom 产生出一个HelloClass 類别,在其中加入一个方法:SayHello ,为其定义一个字串型别的传回值,特别注意的是此值是由OpenReader 所传回的TextReader 中讀回來的,OpenReader 会以TextReader 來开启目前处理的档案, 此例中就是class1.ppp( 后详)。编译后将其复制到网站目錄中的bin 目錄下,假设你的网站目錄下并没有bin 目錄,那就自行建一个吧。接着修改web.config 加入程式5 的定义。程式5
<compilation debug="true">
<buildProviders>
<add extension=".ppp" type="TestBuildProvider.MyCSharpBuilder, TestBuildProvider"
appliesTo="Code"/>
</buildProviders>
</compilation>
完成之后建立一个档案class1.ppp,内容如程式6。程式6
hello i am buildprovider,this message is define in class1.ppp.
将这个档案放在网站目錄下的Code 目錄中,没有Code 目錄的话就建一个吧。现在让我们來试试这个BuildProvdier 能否正常运作吧, 在Default.aspx 中放入一个Button,撰写其事件函式,如程式7。程式7
Type FindType()
{ Assembly[] assems = AppDomain.CurrentDomain.GetAssemblies(); foreach(Assembly assem in assems) {
Type t = assem.GetType("TEST.HelloClass");
if (t != null) return t;
}
return null;
}
void Button2_Click(object sender, EventArgs e)
{ Type t = FindType(); if (t != null)
{ | ||
object obj = Activator.CreateInstance(t); | ||
string s = (string)obj.GetType().InvokeMember("SayHello", | ||
BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public, null, obj, new object[]{}); | ||
Button2.Text = s; | ||
} | ||
} |
FindType 函式是为了找寻HelloClass 这个Type 而写的,在网站执行时虽然会载入BuildProvider 所产生的Assemblys, 但是在这裡并无法知道class1.ppp 所产生出來的Assembly 实际名称,自然也就无法取到HelloClass 了,所以用FindType 來搜寻所有的Assemblys 來取得HelloClass。連Assembly 都无法确定了,当然也无法用一般的方式來建立物件及呼叫方法了,所以就只剩下Reflection 可以用了,此处利用Reflection 來建立HelloClass 物件实体,接着呼叫其SayHello 來取回class1.ppp 中的文字。
另一个编译子系统:Expression Builder
Build Provider 是一个蛮不错的设计,设计师可以撰写自订的Provider 來延伸ASP.NET 的编译系统, 但有时候设计师只是需要一个简单的动态决议系统, 而不是一个以档案为基础的编译动作,例如程式8 中所示。 程式8
ConnectionString="<%$ ConnectionStrings:AppConnectionString1 %>"
这是ASP.NET 内建的一项简易设计,<%$ 后的字串在编译时期时会被解译成程式9 的码。
程式9
source1.ConnectionString = ConnectionStringsExpressionBuilder.GetConnectionString("AppConnectionString1");
藉由此设计, 设计师可以将组态档中的值指给某个属性, 达到以外部档案改变应用程式行为的目的, 也可以减少程式重编译的次數, 那这是如何达到的呢? 如果我们要自定这种功能,又该如何做呢?答案已经在程式9 中出现了, 那就是ExpressionBuilder 物件。ASP.NET 2.0 允许设计师如撰写Build Provider 一般撰写自订的ExpressionBuilder 物件, 程式10 是一个简单到不行的范例。 程式10
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.Web.Compilation;
using System.Reflection; using System.ComponentModel; using System.CodeDom; #endregion
namespace TestExpressionBuilder
{ public class MyExpressionBuilder:ExpressionBuilder {
public override System.CodeDom.CodeExpression
GetCodeExpression(System.Web.UI.BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{ return new CodePrimitiveExpression(entry.Expression); }
public MyExpressionBuilder()
{
}
} }
程式中覆载了GetCodeExpression 函式, 此函式会在解译时期被呼叫, 此时必须传回一个CodeExpression 物件,解译器藉此产生出類似程式9 的程式码。要套用这个ExpressionBuilder,web.config 中必须包含程式11 的设定。 程式11
<compilation debug="true">
<expressionBuilders>
<add expressionPrefix="MyExpression"
type="TestExpressionBuilder.MyExpressionBuilder, TestExpressionBuilder"/>
</expressionBuilders>
</compilation>
expressionPrefix 属性代表着ExpressionBuilder 所能解析的Expression 的开头验证字码, 只有符合这个字串的Expression 才会交给MyExpressionBuilder 來处理,程式12 是测试码。程式12
<asp:Button ID="Button2" Runat="server"
Text="<%$ MyExpression:i am expxression builder %>" />
不过可惜的是, 目前的VWD 似乎完全不认識自定义的ExpressionBuilder, 因此无法在设
计时期显示出正确的结果。
后记
.提醒讀者,NET Framework 2.0 仍处于Beta 阶段,这也代表着目前所谈的技术都是未定數,虽然Build Provider 与Expression Builder 技术带给设计师无限的想象空间, 但除了Microsoft 之外没人能确定,最终版本会不会仍然开放这些功能给设计师使用。