Inside ASP.NET 2.0-即时编译系统

摘要: 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.asaxResource 指的是资源档,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 编译。

ManagerProvide rGenerator

承上节,BuildManagerBuildProvider 及其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 取得HelloClassAssembly 都无法确定,当然也无法用一般的方式物件及呼叫方法,所以就只剩下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 的程式码。要套用这个ExpressionBuilderweb.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 之外没人能确定,最终版本会会仍然开放这些功能给设计师使用。